From cf3b8b81ad25f7c4ab55ba2bc98846f76f9005bf Mon Sep 17 00:00:00 2001 From: Yevhen Popok Date: Wed, 17 Jan 2024 11:57:14 +0200 Subject: [PATCH 001/435] Update description for CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 2f710f3e92..4932585bd5 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -308,7 +308,7 @@ cassandra: poll_ms: "${CASSANDRA_QUERY_POLL_MS:50}" # Interval in milliseconds for printing Cassandra query queue statistic rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}" - # set all data type values except target to null for the same ts on save + # When saving a value, set other data types to null (to avoid different values with the same timeseries). set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:false}" # log one of cassandra queries with specified frequency (0 - logging is disabled) print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}" From ce485d4e101b40419b15c45ea9d0a6eb2d546505 Mon Sep 17 00:00:00 2001 From: Yevhen Popok Date: Mon, 29 Jan 2024 15:21:48 +0200 Subject: [PATCH 002/435] Clarify use-case for the "cassandra.query.set_null_values_enabled" parameter --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 4932585bd5..83f2438ded 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -308,7 +308,7 @@ cassandra: poll_ms: "${CASSANDRA_QUERY_POLL_MS:50}" # Interval in milliseconds for printing Cassandra query queue statistic rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}" - # When saving a value, set other data types to null (to avoid different values with the same timeseries). + # When saving a value, set other data types to null (to avoid having multiple telemetry values with the same timeseries). set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:false}" # log one of cassandra queries with specified frequency (0 - logging is disabled) print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}" From 8584edf3abd927b0d694d0736c84114baafa5f34 Mon Sep 17 00:00:00 2001 From: Yevhen Popok Date: Mon, 29 Jan 2024 15:26:11 +0200 Subject: [PATCH 003/435] Fix updated description for the "cassandra.query.set_null_values_enabled" parameter --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 83f2438ded..6c79dc36d2 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -308,7 +308,7 @@ cassandra: poll_ms: "${CASSANDRA_QUERY_POLL_MS:50}" # Interval in milliseconds for printing Cassandra query queue statistic rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}" - # When saving a value, set other data types to null (to avoid having multiple telemetry values with the same timeseries). + # When saving a value, set other data types to null (to avoid having multiple telemetry values with the same timestamp). set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:false}" # log one of cassandra queries with specified frequency (0 - logging is disabled) print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}" From 8bb4e9dc0b0e5ac3567c16c983c2eedef39bc23e Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 16 Apr 2025 18:31:11 +0200 Subject: [PATCH 004/435] TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES retention 1 day, 30 partitions for new deployments --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index d1910399f0..e23a27c1c9 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1644,7 +1644,7 @@ queue: # Kafka properties for Notifications topics notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for JS Executor topics - js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1}" + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:86400000;segment.bytes:52428800;retention.bytes:104857600;partitions:30;min.insync.replicas:1}" # Kafka properties for OTA updates topic ota-updates: "${TB_QUEUE_KAFKA_OTA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" # Kafka properties for Version Control topic From 0efa8301cb50a245ccff54cbc3b8f6d7390bbb5d Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 16 Apr 2025 18:34:37 +0200 Subject: [PATCH 005/435] Confluent TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES retention 1 day --- docker/queue-confluent.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/queue-confluent.env b/docker/queue-confluent.env index 868a135de3..900504c4ea 100644 --- a/docker/queue-confluent.env +++ b/docker/queue-confluent.env @@ -15,4 +15,4 @@ TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:52428800 TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000 TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000 TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000 -TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600 +TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES=retention.ms:86400000;segment.bytes:52428800;retention.bytes:104857600 From 0f17e5f457b2e2ce54a8faaaedd5a095f072f184 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 13 May 2025 15:39:46 +0300 Subject: [PATCH 006/435] AI rule node: draft implementation --- application/pom.xml | 12 ++ .../main/data/upgrade/basic/schema_update.sql | 10 ++ .../server/actors/ActorSystemContext.java | 5 + .../actors/ruleChain/DefaultTbContext.java | 6 + .../controller/AiSettingsController.java | 80 +++++++++++ .../server/service/ai/AiServiceImpl.java | 64 +++++++++ .../server/dao/ai/AiSettingsService.java | 39 ++++++ .../server/common/data/BaseData.java | 6 + .../server/common/data/EntityType.java | 8 +- .../server/common/data/ai/AiSettings.java | 94 +++++++++++++ .../data/edqs/fields/AiSettingsFields.java | 40 ++++++ .../server/common/data/id/AiSettingsId.java | 51 +++++++ .../common/data/id/EntityIdFactory.java | 106 +++++--------- common/proto/src/main/proto/queue.proto | 1 + .../server/dao/ai/AiSettingsDao.java | 34 +++++ .../server/dao/ai/AiSettingsServiceImpl.java | 88 ++++++++++++ .../server/dao/model/ModelConstants.java | 10 ++ .../dao/model/sql/AiSettingsEntity.java | 96 +++++++++++++ .../dao/sql/ai/AiSettingsRepository.java | 51 +++++++ .../server/dao/sql/ai/JpaAiSettingsDao.java | 91 ++++++++++++ .../main/resources/sql/schema-entities.sql | 11 ++ pom.xml | 8 ++ rule-engine/rule-engine-api/pom.xml | 4 + .../rule/engine/api/RuleEngineAiService.java | 26 ++++ .../rule/engine/api/TbContext.java | 2 + rule-engine/rule-engine-components/pom.xml | 4 + .../thingsboard/rule/engine/ai/TbAiNode.java | 129 ++++++++++++++++++ .../rule/engine/ai/TbAiNodeConfiguration.java | 42 ++++++ 28 files changed, 1048 insertions(+), 70 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java create mode 100644 application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java create mode 100644 common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AiSettingsFields.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java create mode 100644 rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAiService.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java diff --git a/application/pom.xml b/application/pom.xml index 557179e918..5be51761f3 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -377,6 +377,18 @@ org.rocksdb rocksdbjni + + dev.langchain4j + langchain4j-open-ai + + + dev.langchain4j + langchain4j-anthropic + + + dev.langchain4j + langchain4j-google-ai-gemini + diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 016e786776..8bd41a9208 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -14,3 +14,13 @@ -- limitations under the License. -- +CREATE TABLE ai_settings ( + id UUID NOT NULL PRIMARY KEY, + created_time BIGINT NOT NULL, + tenant_id UUID NOT NULL, + version BIGINT NOT NULL DEFAULT 1, + name VARCHAR(255) NOT NULL, + provider VARCHAR(255) NOT NULL, + model VARCHAR(255) NOT NULL, + api_key VARCHAR(1000) NOT NULL +); diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 61d9586095..785ac8da9e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -34,6 +34,7 @@ import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.NotificationCenter; +import org.thingsboard.rule.engine.api.RuleEngineAiService; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; @@ -309,6 +310,10 @@ public class ActorSystemContext { @Getter private AuditLogService auditLogService; + @Autowired + @Getter + private RuleEngineAiService aiService; + @Autowired @Getter private EntityViewService entityViewService; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index c07134105c..14bab1a560 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -27,6 +27,7 @@ import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.NotificationCenter; +import org.thingsboard.rule.engine.api.RuleEngineAiService; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; @@ -1012,6 +1013,11 @@ public class DefaultTbContext implements TbContext { return mainCtx.getAuditLogService(); } + @Override + public RuleEngineAiService getAiService() { + return mainCtx.getAiService(); + } + @Override public MqttClientSettings getMqttClientSettings() { return mainCtx.getMqttClientSettings(); diff --git a/application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java new file mode 100644 index 0000000000..0c981a5d2f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java @@ -0,0 +1,80 @@ +/** + * 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 lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +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.RestController; +import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.ai.AiSettingsService; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.UUID; + +// TODO: TbAiSettingsService? + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/ai-settings") +public class AiSettingsController extends BaseController { + + private final AiSettingsService aiSettingsService; + + @PostMapping + public AiSettings saveAiSettings( + @RequestBody AiSettings aiSettings, + + @AuthenticationPrincipal SecurityUser requestingUser + ) { + return aiSettingsService.save(requestingUser.getTenantId(), aiSettings); + } + + @GetMapping("/{aiSettingsId}") + public AiSettings getAiSettingsById( + @PathVariable("aiSettingsId") UUID aiSettingsUuid, + + @AuthenticationPrincipal SecurityUser requestingUser + ) throws ThingsboardException { + return checkNotNull(aiSettingsService.findAiSettingsByTenantIdAndId(requestingUser.getTenantId(), new AiSettingsId(aiSettingsUuid))); + } + + @GetMapping + public PageData getAllAiSettings( + @AuthenticationPrincipal SecurityUser requestingUser + ) { + return aiSettingsService.findAiSettingsByTenantId(requestingUser.getTenantId(), new PageLink(Integer.MAX_VALUE)); + } + + @DeleteMapping("/{aiSettingsId}") + public boolean deleteAiSettingsById( + @PathVariable("aiSettingsId") UUID aiSettingsUuid, + + @AuthenticationPrincipal SecurityUser requestingUser + ) { + return aiSettingsService.deleteByTenantIdAndId(requestingUser.getTenantId(), new AiSettingsId(aiSettingsUuid)); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java new file mode 100644 index 0000000000..6324623177 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ai; + +import dev.langchain4j.model.anthropic.AnthropicChatModel; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; +import dev.langchain4j.model.openai.OpenAiChatModel; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.rule.engine.api.RuleEngineAiService; +import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.ai.AiSettingsService; + +import java.util.NoSuchElementException; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +class AiServiceImpl implements RuleEngineAiService { + + private final AiSettingsService aiSettingsService; + + @Override + public ChatModel configureChatModel(TenantId tenantId, AiSettingsId aiSettingsId) { + Optional aiSettingsOpt = aiSettingsService.findAiSettingsById(tenantId, aiSettingsId); + if (aiSettingsOpt.isEmpty()) { + throw new NoSuchElementException("AI settings with ID: " + aiSettingsId + " were not found"); + } + var aiSettings = aiSettingsOpt.get(); + + return switch (aiSettings.getProvider()) { + case "openai" -> OpenAiChatModel.builder() + .apiKey(aiSettings.getApiKey()) + .modelName(aiSettings.getModel()) + .build(); + case "anthropic" -> AnthropicChatModel.builder() + .apiKey(aiSettings.getApiKey()) + .modelName(aiSettings.getModel()) + .build(); + case "google-ai-gemini" -> GoogleAiGeminiChatModel.builder() + .apiKey(aiSettings.getApiKey()) + .modelName(aiSettings.getModel()) + .build(); + default -> throw new IllegalArgumentException("Unsupported AI provider: " + aiSettings.getProvider()); + }; + } + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java new file mode 100644 index 0000000000..e932901b7d --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java @@ -0,0 +1,39 @@ +/** + * 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 org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.id.AiSettingsId; +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.EntityDaoService; + +import java.util.Optional; + +public interface AiSettingsService extends EntityDaoService { + + AiSettings save(TenantId tenantId, AiSettings aiSettings); + + Optional findAiSettingsById(TenantId tenantId, AiSettingsId aiSettingsId); + + PageData findAiSettingsByTenantId(TenantId tenantId, PageLink pageLink); + + Optional findAiSettingsByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId); + + boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java b/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java index 10ea83397b..19ce1de43d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.UUIDBased; @@ -41,6 +42,11 @@ public abstract class BaseData extends IdBased implement this.createdTime = data.getCreatedTime(); } + @Schema( + description = "Entity creation timestamp in milliseconds since Unix epoch", + example = "1746028547220", + accessMode = Schema.AccessMode.READ_ONLY + ) public long getCreatedTime() { return createdTime; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 93e754eb2c..89bc456fd9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -63,7 +63,13 @@ public enum EntityType { MOBILE_APP(37), MOBILE_APP_BUNDLE(38), CALCULATED_FIELD(39), - CALCULATED_FIELD_LINK(40); + CALCULATED_FIELD_LINK(40), + AI_SETTINGS(41, "ai_settings") { + @Override + public String getNormalName() { + return "AI settings"; + } + }; @Getter private final int protoNumber; // Corresponds to EntityTypeProto diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java new file mode 100644 index 0000000000..4a6168bcf9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java @@ -0,0 +1,94 @@ +/** + * 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; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.HasVersion; +import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.io.Serial; + +@Data +@Builder +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public final class AiSettings extends BaseData implements HasTenantId, HasVersion, HasName { + + @Serial + private static final long serialVersionUID = 9017108678716011604L; + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_ONLY, + description = "JSON object representing the ID of the tenant associated with these AI settings", + example = "e3c4b7d2-5678-4a9b-0c1d-2e3f4a5b6c7d" + ) + TenantId tenantId; + + @Schema( + requiredMode = Schema.RequiredMode.NOT_REQUIRED, + accessMode = Schema.AccessMode.READ_ONLY, + description = "Version of the AI settings; increments automatically whenever the settings are changed", + example = "7", + defaultValue = "1" + ) + Long version; + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_WRITE, + description = "Human-readable name of the AI settings", + example = "Default AI Settings" + ) + String name; + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_WRITE, + description = "Name of the LLM provider, e.g. 'openai', 'anthropic'", + example = "openai" + ) + String provider; + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_WRITE, + description = "Identifier of the LLM model to use, e.g. 'gpt-4o-mini'", + example = "gpt-4o-mini" + ) + String model; + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.WRITE_ONLY, + description = "API key for authenticating with the selected LLM provider", + example = "sk-********************************" + ) + String apiKey; + + public AiSettings(AiSettingsId id) { + super(id); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AiSettingsFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AiSettingsFields.java new file mode 100644 index 0000000000..751c8538f0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AiSettingsFields.java @@ -0,0 +1,40 @@ +/** + * 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.edqs.fields; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +public class AiSettingsFields extends AbstractEntityFields { + + private String provider; + private String model; + private String apiKey; + + public AiSettingsFields(UUID id, long createdTime, UUID tenantId, long version, String provider, String name, String model, String apiKey) { + super(id, createdTime, tenantId, name, version); + this.provider = provider; + this.model = model; + this.apiKey = apiKey; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java new file mode 100644 index 0000000000..f9b6ab74fe --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java @@ -0,0 +1,51 @@ +/** + * 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.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.thingsboard.server.common.data.EntityType; + +import java.io.Serial; +import java.util.UUID; + +public final class AiSettingsId extends UUIDBased implements EntityId { + + @Serial + private static final long serialVersionUID = 3021036138554389754L; + + @JsonCreator + public AiSettingsId(@JsonProperty("id") UUID id) { + super(id); + } + + @Override + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + description = "Entity type of the AI settings, always 'AI_SETTINGS'", + example = "AI_SETTINGS", + allowableValues = "AI_SETTINGS" + ) + public EntityType getEntityType() { + return EntityType.AI_SETTINGS; + } + + public static AiSettingsId fromString(String uuid) { + return new AiSettingsId(UUID.fromString(uuid)); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index f5dd4b12a0..fcdf1e0a1b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -50,75 +50,43 @@ public class EntityIdFactory { } public static EntityId getByTypeAndUuid(EntityType type, UUID uuid) { - switch (type) { - case TENANT: - return TenantId.fromUUID(uuid); - case CUSTOMER: - return new CustomerId(uuid); - case USER: - return new UserId(uuid); - case DASHBOARD: - return new DashboardId(uuid); - case DEVICE: - return new DeviceId(uuid); - case ASSET: - return new AssetId(uuid); - case ALARM: - return new AlarmId(uuid); - case RULE_CHAIN: - return new RuleChainId(uuid); - case RULE_NODE: - return new RuleNodeId(uuid); - case ENTITY_VIEW: - return new EntityViewId(uuid); - case WIDGETS_BUNDLE: - return new WidgetsBundleId(uuid); - case WIDGET_TYPE: - return new WidgetTypeId(uuid); - case DEVICE_PROFILE: - return new DeviceProfileId(uuid); - case ASSET_PROFILE: - return new AssetProfileId(uuid); - case TENANT_PROFILE: - return new TenantProfileId(uuid); - case API_USAGE_STATE: - return new ApiUsageStateId(uuid); - case TB_RESOURCE: - return new TbResourceId(uuid); - case OTA_PACKAGE: - return new OtaPackageId(uuid); - case EDGE: - return new EdgeId(uuid); - case RPC: - return new RpcId(uuid); - case QUEUE: - return new QueueId(uuid); - case NOTIFICATION_TARGET: - return new NotificationTargetId(uuid); - case NOTIFICATION_REQUEST: - return new NotificationRequestId(uuid); - case NOTIFICATION_RULE: - return new NotificationRuleId(uuid); - case NOTIFICATION_TEMPLATE: - return new NotificationTemplateId(uuid); - case NOTIFICATION: - return new NotificationId(uuid); - case QUEUE_STATS: - return new QueueStatsId(uuid); - case OAUTH2_CLIENT: - return new OAuth2ClientId(uuid); - case MOBILE_APP: - return new MobileAppId(uuid); - case DOMAIN: - return new DomainId(uuid); - case MOBILE_APP_BUNDLE: - return new MobileAppBundleId(uuid); - case CALCULATED_FIELD: - return new CalculatedFieldId(uuid); - case CALCULATED_FIELD_LINK: - return new CalculatedFieldLinkId(uuid); - } - throw new IllegalArgumentException("EntityType " + type + " is not supported!"); + return switch (type) { + case TENANT -> TenantId.fromUUID(uuid); + case CUSTOMER -> new CustomerId(uuid); + case USER -> new UserId(uuid); + case DASHBOARD -> new DashboardId(uuid); + case DEVICE -> new DeviceId(uuid); + case ASSET -> new AssetId(uuid); + case ALARM -> new AlarmId(uuid); + case RULE_CHAIN -> new RuleChainId(uuid); + case RULE_NODE -> new RuleNodeId(uuid); + case ENTITY_VIEW -> new EntityViewId(uuid); + case WIDGETS_BUNDLE -> new WidgetsBundleId(uuid); + case WIDGET_TYPE -> new WidgetTypeId(uuid); + case DEVICE_PROFILE -> new DeviceProfileId(uuid); + case ASSET_PROFILE -> new AssetProfileId(uuid); + case TENANT_PROFILE -> new TenantProfileId(uuid); + case API_USAGE_STATE -> new ApiUsageStateId(uuid); + case TB_RESOURCE -> new TbResourceId(uuid); + case OTA_PACKAGE -> new OtaPackageId(uuid); + case EDGE -> new EdgeId(uuid); + case RPC -> new RpcId(uuid); + case QUEUE -> new QueueId(uuid); + case NOTIFICATION_TARGET -> new NotificationTargetId(uuid); + case NOTIFICATION_REQUEST -> new NotificationRequestId(uuid); + case NOTIFICATION_RULE -> new NotificationRuleId(uuid); + case NOTIFICATION_TEMPLATE -> new NotificationTemplateId(uuid); + case NOTIFICATION -> new NotificationId(uuid); + case QUEUE_STATS -> new QueueStatsId(uuid); + case OAUTH2_CLIENT -> new OAuth2ClientId(uuid); + case MOBILE_APP -> new MobileAppId(uuid); + case DOMAIN -> new DomainId(uuid); + case MOBILE_APP_BUNDLE -> new MobileAppBundleId(uuid); + case CALCULATED_FIELD -> new CalculatedFieldId(uuid); + case CALCULATED_FIELD_LINK -> new CalculatedFieldLinkId(uuid); + case AI_SETTINGS -> new AiSettingsId(uuid); + default -> throw new IllegalArgumentException("EntityType " + type + " is not supported!"); + }; } public static EntityId getByEdgeEventTypeAndUuid(EdgeEventType edgeEventType, UUID uuid) { diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 2a97fd35d0..e5a94c21fc 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -63,6 +63,7 @@ enum EntityTypeProto { MOBILE_APP_BUNDLE = 38; CALCULATED_FIELD = 39; CALCULATED_FIELD_LINK = 40; + AI_SETTINGS = 41; } enum ApiUsageRecordKeyProto { diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsDao.java new file mode 100644 index 0000000000..a94131a642 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsDao.java @@ -0,0 +1,34 @@ +/** + * 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 org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.TenantEntityDao; + +import java.util.Optional; + +public interface AiSettingsDao extends Dao, TenantEntityDao { + + Optional findByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId); + + void deleteByTenantId(TenantId tenantId); + + boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java new file mode 100644 index 0000000000..356bf1d19a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java @@ -0,0 +1,88 @@ +/** + * 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 lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.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 java.util.Optional; + +@Service +@RequiredArgsConstructor +class AiSettingsServiceImpl implements AiSettingsService { + + private final AiSettingsDao aiSettingsDao; + + @Override + public AiSettings save(TenantId tenantId, AiSettings aiSettings) { + aiSettings.setTenantId(tenantId); + return aiSettingsDao.saveAndFlush(tenantId, aiSettings); + } + + @Override + public Optional findAiSettingsById(TenantId tenantId, AiSettingsId aiSettingsId) { + return Optional.ofNullable(aiSettingsDao.findById(tenantId, aiSettingsId.getId())); + } + + @Override + public PageData findAiSettingsByTenantId(TenantId tenantId, PageLink pageLink) { + return aiSettingsDao.findAllByTenantId(tenantId, pageLink); + } + + @Override + public Optional findAiSettingsByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { + return aiSettingsDao.findByTenantIdAndId(tenantId, aiSettingsId); + } + + @Override + public boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { + return aiSettingsDao.deleteByTenantIdAndId(tenantId, aiSettingsId); + } + + @Override + public Optional> findEntity(TenantId tenantId, EntityId entityId) { + return Optional.ofNullable(aiSettingsDao.findById(tenantId, entityId.getId())); + } + + @Override + public long countByTenantId(TenantId tenantId) { + return aiSettingsDao.countByTenantId(tenantId); + } + + @Override + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + aiSettingsDao.removeById(tenantId, id.getId()); + } + + @Override + public void deleteByTenantId(TenantId tenantId) { + aiSettingsDao.deleteByTenantId(tenantId); + } + + @Override + public EntityType getEntityType() { + return EntityType.AI_SETTINGS; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 148908d063..ac09ce088b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -739,6 +739,16 @@ public class ModelConstants { public static final String CALCULATED_FIELD_LINK_ENTITY_ID = ENTITY_ID_COLUMN; public static final String CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID = "calculated_field_id"; + /** + * AI settings constants. + */ + public static final String AI_SETTINGS_TABLE_NAME = "ai_settings"; + public static final String AI_SETTINGS_TENANT_ID_COLUMN_NAME = TENANT_ID_COLUMN; + public static final String AI_SETTINGS_NAME_COLUMN_NAME = NAME_PROPERTY; + public static final String AI_SETTINGS_PROVIDER_COLUMN_NAME = "provider"; + public static final String AI_SETTINGS_MODEL_COLUMN_NAME = "model"; + public static final String AI_SETTINGS_API_KEY_COLUMN_NAME = "api_key"; + protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN), max(TS_COLUMN)}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java new file mode 100644 index 0000000000..f238f57d4b --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java @@ -0,0 +1,96 @@ +/** + * 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.model.sql; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.proxy.HibernateProxy; +import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseVersionedEntity; +import org.thingsboard.server.dao.model.ModelConstants; + +import java.util.Objects; +import java.util.UUID; + +@Getter +@Setter +@ToString +@Entity +@Table(name = ModelConstants.AI_SETTINGS_TABLE_NAME) +public class AiSettingsEntity extends BaseVersionedEntity { + + @Column(name = ModelConstants.AI_SETTINGS_TENANT_ID_COLUMN_NAME, nullable = false, columnDefinition = "uuid") + private UUID tenantId; + + @Column(name = ModelConstants.AI_SETTINGS_NAME_COLUMN_NAME, nullable = false) + private String name; + + @Column(name = ModelConstants.AI_SETTINGS_PROVIDER_COLUMN_NAME, nullable = false) + private String provider; + + @Column(name = ModelConstants.AI_SETTINGS_MODEL_COLUMN_NAME, nullable = false) + private String model; + + @Column(name = ModelConstants.AI_SETTINGS_API_KEY_COLUMN_NAME, nullable = false) + private String apiKey; + + public AiSettingsEntity() {} + + public AiSettingsEntity(AiSettings aiSettings) { + super(aiSettings); + tenantId = getTenantUuid(aiSettings.getTenantId()); + name = aiSettings.getName(); + provider = aiSettings.getProvider(); + model = aiSettings.getModel(); + apiKey = aiSettings.getApiKey(); + } + + @Override + public AiSettings toData() { + var settings = new AiSettings(new AiSettingsId(id)); + settings.setCreatedTime(createdTime); + settings.setVersion(version); + settings.setTenantId(TenantId.fromUUID(tenantId)); + settings.setName(name); + settings.setProvider(provider); + settings.setModel(model); + settings.setApiKey(apiKey); + return settings; + } + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); + Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + AiSettingsEntity that = (AiSettingsEntity) o; + return getId() != null && Objects.equals(getId(), that.getId()); + } + + @Override + public final int hashCode() { + return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java new file mode 100644 index 0000000000..b64fc99bcb --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java @@ -0,0 +1,51 @@ +/** + * 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.sql.ai; + +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.edqs.fields.AiSettingsFields; +import org.thingsboard.server.dao.model.sql.AiSettingsEntity; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface AiSettingsRepository extends JpaRepository { + + Page findByTenantId(UUID tenantId, Pageable pageable); + + Optional findByTenantIdAndId(UUID tenantId, UUID id); + + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.AiSettingsFields(" + + "ai.id, ai.createdTime, ai.tenantId, ai.version, ai.provider, ai.name, ai.model, ai.apiKey) " + + "FROM AiSettingsEntity ai WHERE ai.id > :id ORDER BY ai.id") + List findNextBatch(@Param("id") UUID id, Limit limit); + + Long countByTenantId(UUID tenantId); + + @Transactional + void deleteByTenantId(UUID tenantId); + + @Transactional + boolean deleteByTenantIdAndId(UUID tenantId, UUID id); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java new file mode 100644 index 0000000000..8d08aaefff --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java @@ -0,0 +1,91 @@ +/** + * 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.sql.ai; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Limit; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.edqs.fields.AiSettingsFields; +import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.ai.AiSettingsDao; +import org.thingsboard.server.dao.model.sql.AiSettingsEntity; +import org.thingsboard.server.dao.sql.JpaAbstractDao; +import org.thingsboard.server.dao.util.SqlDao; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@SqlDao +@Component +@RequiredArgsConstructor +class JpaAiSettingsDao extends JpaAbstractDao implements AiSettingsDao { + + private final AiSettingsRepository aiSettingsRepository; + + @Override + public Optional findByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { + return aiSettingsRepository.findByTenantIdAndId(tenantId.getId(), aiSettingsId.getId()).map(DaoUtil::getData); + } + + @Override + public List findNextBatch(UUID id, int batchSize) { + return aiSettingsRepository.findNextBatch(id, Limit.of(batchSize)); + } + + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(aiSettingsRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + } + + @Override + public Long countByTenantId(TenantId tenantId) { + return aiSettingsRepository.countByTenantId(tenantId.getId()); + } + + @Override + public void deleteByTenantId(TenantId tenantId) { + aiSettingsRepository.deleteByTenantId(tenantId.getId()); + } + + @Override + public boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { + return aiSettingsRepository.deleteByTenantIdAndId(tenantId.getId(), aiSettingsId.getId()); + } + + @Override + public EntityType getEntityType() { + return EntityType.AI_SETTINGS; + } + + @Override + protected Class getEntityClass() { + return AiSettingsEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return aiSettingsRepository; + } + +} diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index b425550e7e..aaff15b2d4 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -948,3 +948,14 @@ CREATE TABLE IF NOT EXISTS cf_debug_event ( e_result varchar, e_error varchar ) PARTITION BY RANGE (ts); + +CREATE TABLE IF NOT EXISTS ai_settings ( + id UUID NOT NULL PRIMARY KEY, + created_time BIGINT NOT NULL, + tenant_id UUID NOT NULL, + version BIGINT NOT NULL DEFAULT 1, + name VARCHAR(255) NOT NULL, + provider VARCHAR(255) NOT NULL, + model VARCHAR(255) NOT NULL, + api_key VARCHAR(1000) NOT NULL +); diff --git a/pom.xml b/pom.xml index 6082745758..8f92b66906 100755 --- a/pom.xml +++ b/pom.xml @@ -134,6 +134,7 @@ 1.7.5 3.8.0 2.9.0 + 1.0.0-beta4 4.2.1 2.7.3 @@ -2322,6 +2323,13 @@ rocksdbjni ${rocksdbjni.version} + + dev.langchain4j + langchain4j-bom + ${langchain4j.version} + pom + import + diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index d943759257..cbd6df2d28 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -98,6 +98,10 @@ jakarta.mail provided + + dev.langchain4j + langchain4j + org.springframework.boot spring-boot-starter-test diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAiService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAiService.java new file mode 100644 index 0000000000..ae455d3b51 --- /dev/null +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAiService.java @@ -0,0 +1,26 @@ +/** + * 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.rule.engine.api; + +import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.TenantId; + +public interface RuleEngineAiService { + + ChatModel configureChatModel(TenantId tenantId, AiSettingsId aiSettingsId); + +} diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 7989b8f9ce..1fc7432a08 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -417,6 +417,8 @@ public interface TbContext { AuditLogService getAuditLogService(); + RuleEngineAiService getAiService(); + // Configuration parameters for the MQTT client that is used in the MQTT node and Azure IoT hub node MqttClientSettings getMqttClientSettings(); diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 9666fa8059..26dc2a9004 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -153,6 +153,10 @@ com.jayway.jsonpath json-path + + dev.langchain4j + langchain4j + diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java new file mode 100644 index 0000000000..e148e0397a --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -0,0 +1,129 @@ +/** + * 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.rule.engine.ai; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.ListenableFuture; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.request.ChatRequest; +import dev.langchain4j.model.chat.request.ResponseFormat; +import dev.langchain4j.model.input.PromptTemplate; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +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.plugin.ComponentType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.dao.exception.DataValidationException; + +import java.util.List; +import java.util.Map; + +import static com.google.common.util.concurrent.Futures.addCallback; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static java.util.Objects.requireNonNullElse; +import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields; + +@RuleNode( + type = ComponentType.EXTERNAL, + name = "AI", + nodeDescription = "Interact with AI", + nodeDetails = "This node makes requests to LLM based on a prompt and a input message and returns a response in a form of output message", + configClazz = TbAiNodeConfiguration.class +) +public final class TbAiNode extends TbAbstractExternalNode implements TbNode { + + private static final SystemMessage SYSTEM_MESSAGE = SystemMessage.from(""" + Take a deep breath and work on this step by step. + You are an industry-leading IoT domain expert with deep experience in telemetry data analysis. + Your task is to complete the user-provided task or answer a question. + You may use additional context information called "Rule engine message payload", "Rule engine message metadata" and "Rule engine message type". + Your response must be in JSON format."""); + + private TbAiNodeConfiguration config; + + private PromptTemplate userPromptTemplate; + private ChatModel chatModel; + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + config = TbNodeUtils.convert(configuration, TbAiNodeConfiguration.class); + String errorPrefix = "'" + ctx.getSelf().getName() + "' node configuration is invalid: "; + try { + validateFields(config, errorPrefix); + } catch (DataValidationException e) { + throw new TbNodeException(e, true); + } + userPromptTemplate = PromptTemplate.from(""" + User-provided task or question: %s + Rule engine message payload: {{msgPayload}} + Rule engine message metadata: {{msgMetadata}} + Rule engine message type: {{msgType}}""" + .formatted(config.getUserPrompt()) + ); + chatModel = ctx.getAiService().configureChatModel(ctx.getTenantId(), config.getAiSettingsId()); + } + + @Override + public void onMsg(TbContext ctx, TbMsg msg) { + var ackedMsg = ackIfNeeded(ctx, msg); + + Map variables = Map.of( + "msgPayload", msg.getData(), + "msgMetadata", requireNonNullElse(JacksonUtil.toString(msg.getMetaData().getData()), "{}"), + "msgType", msg.getType() + ); + UserMessage userMessage = userPromptTemplate.apply(variables).toUserMessage(); + + var chatRequest = ChatRequest.builder() + .messages(List.of(SYSTEM_MESSAGE, userMessage)) + .responseFormat(ResponseFormat.JSON) + .build(); + + addCallback(sendChatRequest(ctx, chatRequest), new FutureCallback<>() { + @Override + public void onSuccess(String response) { + tellSuccess(ctx, ackedMsg.transform() + .data(response) + .build()); + } + + @Override + public void onFailure(@NonNull Throwable t) { + tellFailure(ctx, ackedMsg, t); + } + }, directExecutor()); + } + + private ListenableFuture sendChatRequest(TbContext ctx, ChatRequest chatRequest) { + return ctx.getExternalCallExecutor().submit(() -> chatModel.chat(chatRequest).aiMessage().text()); + } + + @Override + public void destroy() { + config = null; + userPromptTemplate = null; + chatModel = null; + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java new file mode 100644 index 0000000000..f7d45e1b76 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -0,0 +1,42 @@ +/** + * 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.rule.engine.ai; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.validation.Length; + +@Data +public class TbAiNodeConfiguration implements NodeConfiguration { + + @NotNull + private AiSettingsId aiSettingsId; + + @NotBlank + @Length(min = 1, max = 1000) + private String userPrompt; + + @Override + public TbAiNodeConfiguration defaultConfiguration() { + var configuration = new TbAiNodeConfiguration(); + configuration.setUserPrompt("Tell me a joke"); + return configuration; + } + +} From 9ef4295d906b0c14b5164e653b3d1e23cbfe87a1 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 15 May 2025 18:49:21 +0300 Subject: [PATCH 007/435] AI rule node: bump langchain4j version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8f92b66906..074bbbce03 100755 --- a/pom.xml +++ b/pom.xml @@ -134,7 +134,7 @@ 1.7.5 3.8.0 2.9.0 - 1.0.0-beta4 + 1.0.0 4.2.1 2.7.3 From c0480ed9302ce2e89152cd3f454bc7db250580ae Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 15 May 2025 18:50:00 +0300 Subject: [PATCH 008/435] AI rule node: move system prompt to rule node config --- .../thingsboard/rule/engine/ai/TbAiNode.java | 17 +++++------------ .../rule/engine/ai/TbAiNodeConfiguration.java | 10 ++++++++++ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index e148e0397a..5682529224 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -53,27 +53,20 @@ import static org.thingsboard.server.dao.service.ConstraintValidator.validateFie ) public final class TbAiNode extends TbAbstractExternalNode implements TbNode { - private static final SystemMessage SYSTEM_MESSAGE = SystemMessage.from(""" - Take a deep breath and work on this step by step. - You are an industry-leading IoT domain expert with deep experience in telemetry data analysis. - Your task is to complete the user-provided task or answer a question. - You may use additional context information called "Rule engine message payload", "Rule engine message metadata" and "Rule engine message type". - Your response must be in JSON format."""); - - private TbAiNodeConfiguration config; - + private SystemMessage systemMessage; private PromptTemplate userPromptTemplate; private ChatModel chatModel; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - config = TbNodeUtils.convert(configuration, TbAiNodeConfiguration.class); + var config = TbNodeUtils.convert(configuration, TbAiNodeConfiguration.class); String errorPrefix = "'" + ctx.getSelf().getName() + "' node configuration is invalid: "; try { validateFields(config, errorPrefix); } catch (DataValidationException e) { throw new TbNodeException(e, true); } + systemMessage = SystemMessage.from(config.getSystemPrompt()); userPromptTemplate = PromptTemplate.from(""" User-provided task or question: %s Rule engine message payload: {{msgPayload}} @@ -96,7 +89,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { UserMessage userMessage = userPromptTemplate.apply(variables).toUserMessage(); var chatRequest = ChatRequest.builder() - .messages(List.of(SYSTEM_MESSAGE, userMessage)) + .messages(List.of(systemMessage, userMessage)) .responseFormat(ResponseFormat.JSON) .build(); @@ -121,7 +114,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { @Override public void destroy() { - config = null; + systemMessage = null; userPromptTemplate = null; chatModel = null; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index f7d45e1b76..9d8ac5fbd1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -28,6 +28,10 @@ public class TbAiNodeConfiguration implements NodeConfiguration Date: Thu, 15 May 2025 18:51:53 +0300 Subject: [PATCH 009/435] AI rule node: remove `findNextBatch()` method --- .../data/edqs/fields/AiSettingsFields.java | 40 ------------------- .../dao/sql/ai/AiSettingsRepository.java | 7 +--- 2 files changed, 1 insertion(+), 46 deletions(-) delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AiSettingsFields.java diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AiSettingsFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AiSettingsFields.java deleted file mode 100644 index 751c8538f0..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AiSettingsFields.java +++ /dev/null @@ -1,40 +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.edqs.fields; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -import java.util.UUID; - -@Data -@EqualsAndHashCode(callSuper = true) -@NoArgsConstructor -public class AiSettingsFields extends AbstractEntityFields { - - private String provider; - private String model; - private String apiKey; - - public AiSettingsFields(UUID id, long createdTime, UUID tenantId, long version, String provider, String name, String model, String apiKey) { - super(id, createdTime, tenantId, name, version); - this.provider = provider; - this.model = model; - this.apiKey = apiKey; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java index b64fc99bcb..109557095d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java @@ -35,12 +35,7 @@ public interface AiSettingsRepository extends JpaRepository findByTenantIdAndId(UUID tenantId, UUID id); - @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.AiSettingsFields(" + - "ai.id, ai.createdTime, ai.tenantId, ai.version, ai.provider, ai.name, ai.model, ai.apiKey) " + - "FROM AiSettingsEntity ai WHERE ai.id > :id ORDER BY ai.id") - List findNextBatch(@Param("id") UUID id, Limit limit); - - Long countByTenantId(UUID tenantId); + long countByTenantId(UUID tenantId); @Transactional void deleteByTenantId(UUID tenantId); From 1a3b1c3fe499fe174ac3fc98668c3f2b1dd13277 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 15 May 2025 18:52:31 +0300 Subject: [PATCH 010/435] AI rule node: REST API for CRUD operations on AI settings --- .../controller/AiSettingsController.java | 123 ++++++++++++++---- .../server/controller/BaseController.java | 13 ++ .../controller/ControllerConstants.java | 3 +- .../service/security/permission/Resource.java | 3 +- .../permission/TenantAdminPermissions.java | 17 +++ .../server/dao/ai/AiSettingsService.java | 2 +- .../server/common/data/ai/AiSettings.java | 6 +- .../server/dao/ai/AiSettingsServiceImpl.java | 8 +- .../dao/sql/ai/AiSettingsRepository.java | 14 +- .../server/dao/sql/ai/JpaAiSettingsDao.java | 15 +-- 10 files changed, 153 insertions(+), 51 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java index 0c981a5d2f..2f7aa1dd43 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java @@ -15,66 +15,135 @@ */ package org.thingsboard.server.controller; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.annotation.AuthenticationPrincipal; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import org.springframework.security.access.prepost.PreAuthorize; 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.AiSettings; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AiSettingsId; import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.ai.AiSettingsService; -import org.thingsboard.server.service.security.model.SecurityUser; +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.Set; import java.util.UUID; -// TODO: TbAiSettingsService? +import static org.thingsboard.server.controller.ControllerConstants.AI_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; @RestController -@RequiredArgsConstructor @RequestMapping("/api/ai-settings") public class AiSettingsController extends BaseController { - private final AiSettingsService aiSettingsService; + private static final Set ALLOWED_SORT_PROPERTIES = Set.of("createdTime", "name", "provider", "model"); + @ApiOperation( + value = "Create or update AI settings (saveAiSettings)", + notes = "Creates or updates an AI settings record.\n\n" + + "• **Create:** Omit the `id` to create a new record. The platform assigns a UUID to the new settings and returns it in the `id` field of the response.\n\n" + + "• **Update:** Include an existing `id` to modify that record. If no matching record exists, the API responds with **404 Not Found**.\n\n" + + "Tenant ID for the AI settings will be taken from the authenticated user making the request, regardless of any value provided in the request body." + + TENANT_AUTHORITY_PARAGRAPH + ) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping - public AiSettings saveAiSettings( - @RequestBody AiSettings aiSettings, - - @AuthenticationPrincipal SecurityUser requestingUser - ) { - return aiSettingsService.save(requestingUser.getTenantId(), aiSettings); + public AiSettings saveAiSettings(@RequestBody AiSettings aiSettings) throws ThingsboardException { + aiSettings.setTenantId(getTenantId()); + checkEntity(aiSettings.getId(), aiSettings, Resource.AI_SETTINGS); + return aiSettingsService.save(aiSettings); } + @ApiOperation( + value = "Get AI settings by ID (getAiSettingsById)", + notes = "Fetches an AI settings record by its `id`." + + TENANT_AUTHORITY_PARAGRAPH + ) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping("/{aiSettingsId}") public AiSettings getAiSettingsById( - @PathVariable("aiSettingsId") UUID aiSettingsUuid, - - @AuthenticationPrincipal SecurityUser requestingUser + @Parameter( + description = "ID of the AI settings record", + required = true, + example = "de7900d4-30e2-11f0-9cd2-0242ac120002" + ) + @PathVariable("aiSettingsId") UUID aiSettingsUuid ) throws ThingsboardException { - return checkNotNull(aiSettingsService.findAiSettingsByTenantIdAndId(requestingUser.getTenantId(), new AiSettingsId(aiSettingsUuid))); + return checkAiSettingsId(new AiSettingsId(aiSettingsUuid), Operation.READ); } + @ApiOperation( + value = "Get AI settings (getAiSettings)", + notes = "Returns a page of AI settings. " + + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH + ) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping - public PageData getAllAiSettings( - @AuthenticationPrincipal SecurityUser requestingUser - ) { - return aiSettingsService.findAiSettingsByTenantId(requestingUser.getTenantId(), new PageLink(Integer.MAX_VALUE)); + public PageData getAiSettings( + @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = AI_SETTINGS_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "provider", "model"})) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder + ) throws ThingsboardException { + var user = getCurrentUser(); + accessControlService.checkPermission(user, Resource.AI_SETTINGS, Operation.READ); + validateSortProperty(sortProperty); + var pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return aiSettingsService.findAiSettingsByTenantId(user.getTenantId(), pageLink); } + private static void validateSortProperty(String sortProperty) { + if (sortProperty != null && !ALLOWED_SORT_PROPERTIES.contains(sortProperty)) { + throw new IllegalArgumentException("Unsupported sort property '" + sortProperty + "'! Only '" + String.join("', '", ALLOWED_SORT_PROPERTIES) + "' are allowed."); + } + } + + @ApiOperation( + value = "Delete AI settings by ID (deleteAiSettingsById)", + notes = "Deletes the AI settings record by its `id`. " + + "If a record with the specified `id` exists, the record is deleted and the endpoint returns `true`. " + + "If no such record exists, the endpoint returns `false`." + + TENANT_AUTHORITY_PARAGRAPH + ) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @DeleteMapping("/{aiSettingsId}") public boolean deleteAiSettingsById( - @PathVariable("aiSettingsId") UUID aiSettingsUuid, - - @AuthenticationPrincipal SecurityUser requestingUser - ) { - return aiSettingsService.deleteByTenantIdAndId(requestingUser.getTenantId(), new AiSettingsId(aiSettingsUuid)); + @Parameter( + description = "ID of the AI settings record", + required = true, + example = "de7900d4-30e2-11f0-9cd2-0242ac120002" + ) + @PathVariable("aiSettingsId") UUID aiSettingsUuid + ) throws ThingsboardException { + var user = getCurrentUser(); + var aiSettingsId = new AiSettingsId(aiSettingsUuid); + accessControlService.checkPermission(user, Resource.AI_SETTINGS, Operation.DELETE); + Optional aiSettingsOpt = aiSettingsService.findAiSettingsByTenantIdAndId(user.getTenantId(), aiSettingsId); + if (aiSettingsOpt.isEmpty()) { + return false; + } + accessControlService.checkPermission(user, Resource.AI_SETTINGS, Operation.DELETE, aiSettingsId, aiSettingsOpt.get()); + return aiSettingsService.deleteByTenantIdAndId(user.getTenantId(), aiSettingsId); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 73e278389a..ef50a6552c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -63,6 +63,7 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.ai.AiSettings; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmInfo; @@ -77,6 +78,7 @@ import org.thingsboard.server.common.data.edge.EdgeInfo; import org.thingsboard.server.common.data.exception.EntityVersionMismatchException; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AiSettingsId; import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; @@ -129,6 +131,7 @@ import org.thingsboard.server.common.data.util.ThrowingBiFunction; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.ai.AiSettingsService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -373,6 +376,9 @@ public abstract class BaseController { @Autowired protected CalculatedFieldService calculatedFieldService; + @Autowired + protected AiSettingsService aiSettingsService; + @Value("${server.log_controller_error_stack_trace}") @Getter private boolean logControllerErrorStackTrace; @@ -681,6 +687,9 @@ public abstract class BaseController { case CALCULATED_FIELD: checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation); return; + case AI_SETTINGS: + checkAiSettingsId(new AiSettingsId(entityId.getId()), operation); + return; default: checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation); } @@ -881,6 +890,10 @@ public abstract class BaseController { return checkEntityId(notificationTargetId, notificationTargetService::findNotificationTargetById, operation); } + AiSettings checkAiSettingsId(AiSettingsId aiSettingsId, Operation operation) throws ThingsboardException { + return checkEntityId(aiSettingsId, (tenantId, id) -> aiSettingsService.findAiSettingsByTenantIdAndId(tenantId, id).orElse(null), operation); + } + protected I emptyId(EntityType entityType) { return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); } diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index 8817c24efe..097cb4e577 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -31,7 +31,7 @@ public class ControllerConstants { protected static final String ASSIGNEE_ID = "assigneeId"; protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " + "The result is wrapped with PageData object that allows you to iterate over result set using pagination. " + - "See the 'Model' tab of the Response Class for more details. "; + "See response schema for more details. "; protected static final String INLINE_IMAGES = "inlineImages"; protected static final String INLINE_IMAGES_DESCRIPTION = "Inline images as a data URL (Base64)"; @@ -90,6 +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_SETTINGS_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the AI settings name"; protected static final String ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset profile name."; protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the customer title."; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index 9d7590f786..55b7577ebe 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -51,7 +51,8 @@ public enum Resource { NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE), MOBILE_APP_SETTINGS, - CALCULATED_FIELD(EntityType.CALCULATED_FIELD); + CALCULATED_FIELD(EntityType.CALCULATED_FIELD), + AI_SETTINGS(EntityType.AI_SETTINGS); private final Set entityTypes; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index a072cf2738..990f29798c 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -18,6 +18,8 @@ package org.thingsboard.server.service.security.permission; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.id.AiSettingsId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; @@ -56,6 +58,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.MOBILE_APP, tenantEntityPermissionChecker); put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker); put(Resource.CALCULATED_FIELD, tenantEntityPermissionChecker); + put(Resource.AI_SETTINGS, aiSettingsPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { @@ -146,4 +149,18 @@ public class TenantAdminPermissions extends AbstractPermissions { }; + private static final PermissionChecker aiSettingsPermissionChecker = new PermissionChecker<>() { + + @Override + public boolean hasPermission(SecurityUser user, Operation operation) { + return true; + } + + @Override + public boolean hasPermission(SecurityUser user, Operation operation, AiSettingsId entityId, AiSettings entity) { + return user.getTenantId().equals(entity.getTenantId()); + } + + }; + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java index e932901b7d..f06923b008 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java @@ -26,7 +26,7 @@ import java.util.Optional; public interface AiSettingsService extends EntityDaoService { - AiSettings save(TenantId tenantId, AiSettings aiSettings); + AiSettings save(AiSettings aiSettings); Optional findAiSettingsById(TenantId tenantId, AiSettingsId aiSettingsId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java index 4a6168bcf9..64a2231ef7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java @@ -47,7 +47,7 @@ public final class AiSettings extends BaseData implements HasTenan TenantId tenantId; @Schema( - requiredMode = Schema.RequiredMode.NOT_REQUIRED, + requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "Version of the AI settings; increments automatically whenever the settings are changed", example = "7", @@ -66,7 +66,7 @@ public final class AiSettings extends BaseData implements HasTenan @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_WRITE, - description = "Name of the LLM provider, e.g. 'openai', 'anthropic'", + description = "Name of the LLM provider", example = "openai" ) String provider; @@ -74,7 +74,7 @@ public final class AiSettings extends BaseData implements HasTenan @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_WRITE, - description = "Identifier of the LLM model to use, e.g. 'gpt-4o-mini'", + description = "Identifier of the LLM model to use", example = "gpt-4o-mini" ) String model; diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java index 356bf1d19a..35710819a6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java @@ -28,6 +28,8 @@ import org.thingsboard.server.common.data.page.PageLink; import java.util.Optional; +import static org.thingsboard.server.dao.service.Validator.validatePageLink; + @Service @RequiredArgsConstructor class AiSettingsServiceImpl implements AiSettingsService { @@ -35,9 +37,8 @@ class AiSettingsServiceImpl implements AiSettingsService { private final AiSettingsDao aiSettingsDao; @Override - public AiSettings save(TenantId tenantId, AiSettings aiSettings) { - aiSettings.setTenantId(tenantId); - return aiSettingsDao.saveAndFlush(tenantId, aiSettings); + public AiSettings save(AiSettings aiSettings) { + return aiSettingsDao.saveAndFlush(aiSettings.getTenantId(), aiSettings); } @Override @@ -47,6 +48,7 @@ class AiSettingsServiceImpl implements AiSettingsService { @Override public PageData findAiSettingsByTenantId(TenantId tenantId, PageLink pageLink) { + validatePageLink(pageLink); return aiSettingsDao.findAllByTenantId(tenantId, pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java index 109557095d..e52177d247 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java @@ -15,23 +15,25 @@ */ package org.thingsboard.server.dao.sql.ai; -import org.springframework.data.domain.Limit; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; -import org.thingsboard.server.common.data.edqs.fields.AiSettingsFields; import org.thingsboard.server.dao.model.sql.AiSettingsEntity; -import java.util.List; import java.util.Optional; import java.util.UUID; public interface AiSettingsRepository extends JpaRepository { - Page findByTenantId(UUID tenantId, Pageable pageable); + @Query("SELECT ai " + + "FROM AiSettingsEntity ai " + + "WHERE ai.tenantId = :tenantId " + + "AND (:textSearch IS NULL OR ilike(ai.name, CONCAT('%', :textSearch, '%')) = true)") + Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); Optional findByTenantIdAndId(UUID tenantId, UUID id); @@ -41,6 +43,8 @@ public interface AiSettingsRepository extends JpaRepository impl return aiSettingsRepository.findByTenantIdAndId(tenantId.getId(), aiSettingsId.getId()).map(DaoUtil::getData); } - @Override - public List findNextBatch(UUID id, int batchSize) { - return aiSettingsRepository.findNextBatch(id, Limit.of(batchSize)); - } - @Override public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { - return DaoUtil.toPageData(aiSettingsRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + return DaoUtil.toPageData(aiSettingsRepository.findByTenantId( + tenantId.getId(), StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), DaoUtil.toPageable(pageLink)) + ); } @Override @@ -70,7 +65,7 @@ class JpaAiSettingsDao extends JpaAbstractDao impl @Override public boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { - return aiSettingsRepository.deleteByTenantIdAndId(tenantId.getId(), aiSettingsId.getId()); + return aiSettingsRepository.deleteByTenantIdAndId(tenantId.getId(), aiSettingsId.getId()) > 0; } @Override From 4212f9dde469ed6615b1caec542f40e9748c8ca6 Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Fri, 16 May 2025 15:19:01 +0300 Subject: [PATCH 011/435] Fix for widgetTitlePanel templateOutlet --- .../lib/chart/bar-chart-with-labels-widget.component.html | 2 +- .../lib/chart/bar-chart-with-labels-widget.component.ts | 3 +++ .../widget/lib/chart/range-chart-widget.component.html | 2 +- .../widget/lib/chart/range-chart-widget.component.ts | 3 +++ .../widget/lib/chart/time-series-chart-widget.component.html | 2 +- .../widget/lib/chart/time-series-chart-widget.component.ts | 3 +++ .../home/components/widget/lib/maps/map-widget.component.html | 2 +- .../home/components/widget/lib/maps/map-widget.component.ts | 4 ++++ 8 files changed, 17 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html index 0e0170c603..3f4835f475 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html @@ -19,7 +19,7 @@
@if (widgetComponent.dashboardWidget.showWidgetTitlePanel) {
- +
} @else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts index 746daf43b3..14e77ae738 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts @@ -58,6 +58,9 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft @Input() ctx: WidgetContext; + @Input() + widgetTitlePanel: TemplateRef; + showLegend: boolean; legendClass: string; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html index 39112c484b..fc16fbdc13 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html @@ -19,7 +19,7 @@
@if (widgetComponent.dashboardWidget.showWidgetTitlePanel) {
- +
} @else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts index 0201ec7d5f..92bc8e887f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts @@ -66,6 +66,9 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn @Input() ctx: WidgetContext; + @Input() + widgetTitlePanel: TemplateRef; + showLegend: boolean; legendClass: string; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html index 1efcf30781..fe7335c92d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html @@ -19,7 +19,7 @@
@if (widgetComponent.dashboardWidget.showWidgetTitlePanel) {
- +
} @else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts index 2fd12b1b98..2e9f297b4a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts @@ -61,6 +61,9 @@ export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterV @Input() ctx: WidgetContext; + @Input() + widgetTitlePanel: TemplateRef; + horizontalLegendPosition = false; showLegend: boolean; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html index e64ee71170..c0aceeabb0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html @@ -19,7 +19,7 @@
@if (widgetComponent.dashboardWidget.showWidgetTitlePanel) {
- +
} @else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts index 4ef3c3cd45..013a6c88a9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts @@ -21,6 +21,7 @@ import { Input, OnDestroy, OnInit, + TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core'; @@ -54,6 +55,9 @@ export class MapWidgetComponent implements OnInit, OnDestroy { @Input() ctx: WidgetContext; + @Input() + widgetTitlePanel: TemplateRef; + backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; padding: string; From 18c75998efc3903190c33680c411efca73b84a27 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 16 May 2025 15:21:17 +0300 Subject: [PATCH 012/435] AI rule node: replace Anthropic with Mistral AI --- application/pom.xml | 4 ++-- .../java/org/thingsboard/server/service/ai/AiServiceImpl.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index 5be51761f3..976ec99fbe 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -383,11 +383,11 @@ dev.langchain4j - langchain4j-anthropic + langchain4j-google-ai-gemini dev.langchain4j - langchain4j-google-ai-gemini + langchain4j-mistral-ai diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java index 6324623177..b7c91de260 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java @@ -15,9 +15,9 @@ */ package org.thingsboard.server.service.ai; -import dev.langchain4j.model.anthropic.AnthropicChatModel; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; +import dev.langchain4j.model.mistralai.MistralAiChatModel; import dev.langchain4j.model.openai.OpenAiChatModel; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -49,7 +49,7 @@ class AiServiceImpl implements RuleEngineAiService { .apiKey(aiSettings.getApiKey()) .modelName(aiSettings.getModel()) .build(); - case "anthropic" -> AnthropicChatModel.builder() + case "mistral-ai" -> MistralAiChatModel.builder() .apiKey(aiSettings.getApiKey()) .modelName(aiSettings.getModel()) .build(); From e695ce32837487ea9796e02c49c525d0ecc9ac14 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 16 May 2025 15:26:43 +0300 Subject: [PATCH 013/435] AI rule node: make name of the AI settings unique within the scope of the tenant --- application/src/main/data/upgrade/basic/schema_update.sql | 3 ++- .../org/thingsboard/server/common/data/ai/AiSettings.java | 2 +- .../thingsboard/server/dao/ai/AiSettingsServiceImpl.java | 8 +++++++- dao/src/main/resources/sql/schema-entities.sql | 3 ++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 8bd41a9208..f25ca02939 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -22,5 +22,6 @@ CREATE TABLE ai_settings ( name VARCHAR(255) NOT NULL, provider VARCHAR(255) NOT NULL, model VARCHAR(255) NOT NULL, - api_key VARCHAR(1000) NOT NULL + api_key VARCHAR(1000) NOT NULL, + CONSTRAINT ai_settings_name_unq_key UNIQUE (tenant_id, name) ); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java index 64a2231ef7..162828da8d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java @@ -58,7 +58,7 @@ public final class AiSettings extends BaseData implements HasTenan @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_WRITE, - description = "Human-readable name of the AI settings", + description = "Human-readable name of the AI settings; must be unique within the scope of the tenant", example = "Default AI Settings" ) String name; diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java index 35710819a6..ca23b6bc56 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.page.PageLink; import java.util.Optional; +import static org.thingsboard.server.dao.entity.AbstractEntityService.checkConstraintViolation; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @Service @@ -38,7 +39,12 @@ class AiSettingsServiceImpl implements AiSettingsService { @Override public AiSettings save(AiSettings aiSettings) { - return aiSettingsDao.saveAndFlush(aiSettings.getTenantId(), aiSettings); + try { + return aiSettingsDao.saveAndFlush(aiSettings.getTenantId(), aiSettings); + } catch (Exception e) { + checkConstraintViolation(e, "ai_settings_name_unq_key", "AI settings record with such name already exists!"); + throw e; + } } @Override diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index aaff15b2d4..9d5ae69c95 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -957,5 +957,6 @@ CREATE TABLE IF NOT EXISTS ai_settings ( name VARCHAR(255) NOT NULL, provider VARCHAR(255) NOT NULL, model VARCHAR(255) NOT NULL, - api_key VARCHAR(1000) NOT NULL + api_key VARCHAR(1000) NOT NULL, + CONSTRAINT ai_settings_name_unq_key UNIQUE (tenant_id, name) ); From a3a1971a6851bcb5b2daa11374be0686806a47e8 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 16 May 2025 15:37:13 +0300 Subject: [PATCH 014/435] AI rule node: add support for AI settings in TenantIdLoader --- .../thingsboard/server/actors/ActorSystemContext.java | 5 +++++ .../server/actors/ruleChain/DefaultTbContext.java | 6 ++++++ .../thingsboard/server/common/data/ai/AiSettings.java | 2 ++ .../org/thingsboard/rule/engine/api/TbContext.java | 3 +++ .../thingsboard/rule/engine/util/TenantIdLoader.java | 4 ++++ .../rule/engine/util/TenantIdLoaderTest.java | 11 +++++++++++ 6 files changed, 31 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 785ac8da9e..e420d6a6f6 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -62,6 +62,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.stats.TbApiUsageReportClient; +import org.thingsboard.server.dao.ai.AiSettingsService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -314,6 +315,10 @@ public class ActorSystemContext { @Getter private RuleEngineAiService aiService; + @Autowired + @Getter + private AiSettingsService aiSettingsService; + @Autowired @Getter private EntityViewService entityViewService; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 14bab1a560..3aba1f5379 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -76,6 +76,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgProcessingStackItem; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.ai.AiSettingsService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -1018,6 +1019,11 @@ public class DefaultTbContext implements TbContext { return mainCtx.getAiService(); } + @Override + public AiSettingsService getAiSettingsService() { + return mainCtx.getAiSettingsService(); + } + @Override public MqttClientSettings getMqttClientSettings() { return mainCtx.getMqttClientSettings(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java index 162828da8d..0c5b9c1a63 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java @@ -87,6 +87,8 @@ public final class AiSettings extends BaseData implements HasTenan ) String apiKey; + public AiSettings() {} + public AiSettings(AiSettingsId id) { super(id); } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 1fc7432a08..4f9e786357 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.ai.AiSettingsService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -419,6 +420,8 @@ public interface TbContext { RuleEngineAiService getAiService(); + AiSettingsService getAiSettingsService(); + // Configuration parameters for the MQTT client that is used in the MQTT node and Azure IoT hub node MqttClientSettings getMqttClientSettings(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java index f12a856567..62ca65db1b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -19,6 +19,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; +import org.thingsboard.server.common.data.id.AiSettingsId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.AssetId; @@ -175,6 +176,9 @@ public class TenantIdLoader { tenantEntity = null; } break; + case AI_SETTINGS: + tenantEntity = ctx.getAiSettingsService().findAiSettingsById(ctxTenantId, new AiSettingsId(id)).orElse(null); + break; default: throw new RuntimeException("Unexpected entity type: " + entityId.getEntityType()); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index 38417c3922..c763a1b069 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.ai.AiSettings; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; @@ -68,6 +69,7 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.ai.AiSettingsService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; @@ -92,6 +94,7 @@ import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; +import java.util.Optional; import java.util.UUID; import static org.mockito.ArgumentMatchers.any; @@ -160,6 +163,8 @@ public class TenantIdLoaderTest { private MobileAppBundleService mobileAppBundleService; @Mock private CalculatedFieldService calculatedFieldService; + @Mock + private AiSettingsService aiSettingsService; private TenantId tenantId; private TenantProfileId tenantProfileId; @@ -419,6 +424,12 @@ public class TenantIdLoaderTest { when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService); doReturn(calculatedFieldLink).when(calculatedFieldService).findCalculatedFieldLinkById(eq(tenantId), any()); break; + case AI_SETTINGS: + AiSettings aiSettings = new AiSettings(); + aiSettings.setTenantId(tenantId); + when(ctx.getAiSettingsService()).thenReturn(aiSettingsService); + doReturn(Optional.of(aiSettings)).when(aiSettingsService).findAiSettingsById(eq(tenantId), any()); + break; default: throw new RuntimeException("Unexpected originator EntityType " + entityType); } From ad0161e3dfddd60b50f1ed02b5db4f9562eaf710 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 16 May 2025 16:46:00 +0300 Subject: [PATCH 015/435] AI rule node: add AiProvider enum --- .../server/service/ai/AiServiceImpl.java | 7 +++--- .../server/common/data/ai/AiProvider.java | 24 +++++++++++++++++++ .../server/common/data/ai/AiSettings.java | 12 ++++++---- .../dao/model/sql/AiSettingsEntity.java | 6 ++++- .../thingsboard/rule/engine/ai/TbAiNode.java | 2 +- 5 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/AiProvider.java diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java index b7c91de260..9306ce5c71 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java @@ -45,19 +45,18 @@ class AiServiceImpl implements RuleEngineAiService { var aiSettings = aiSettingsOpt.get(); return switch (aiSettings.getProvider()) { - case "openai" -> OpenAiChatModel.builder() + case OPENAI -> OpenAiChatModel.builder() .apiKey(aiSettings.getApiKey()) .modelName(aiSettings.getModel()) .build(); - case "mistral-ai" -> MistralAiChatModel.builder() + case MISTRAL_AI -> MistralAiChatModel.builder() .apiKey(aiSettings.getApiKey()) .modelName(aiSettings.getModel()) .build(); - case "google-ai-gemini" -> GoogleAiGeminiChatModel.builder() + case GOOGLE_AI_GEMINI -> GoogleAiGeminiChatModel.builder() .apiKey(aiSettings.getApiKey()) .modelName(aiSettings.getModel()) .build(); - default -> throw new IllegalArgumentException("Unsupported AI provider: " + aiSettings.getProvider()); }; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiProvider.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiProvider.java new file mode 100644 index 0000000000..e2938cdddc --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiProvider.java @@ -0,0 +1,24 @@ +/** + * 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; + +public enum AiProvider { + + OPENAI, + GOOGLE_AI_GEMINI, + MISTRAL_AI + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java index 0c5b9c1a63..81a27ae914 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java @@ -66,15 +66,17 @@ public final class AiSettings extends BaseData implements HasTenan @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_WRITE, - description = "Name of the LLM provider", - example = "openai" + description = "Name of the AI provider", + example = "OPENAI", + allowableValues = {"OPENAI", "GOOGLE_AI_GEMINI", "MISTRAL_AI"}, + type = "string" ) - String provider; + AiProvider provider; @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_WRITE, - description = "Identifier of the LLM model to use", + description = "Identifier of the AI model to use", example = "gpt-4o-mini" ) String model; @@ -82,7 +84,7 @@ public final class AiSettings extends BaseData implements HasTenan @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.WRITE_ONLY, - description = "API key for authenticating with the selected LLM provider", + description = "API key for authenticating with the selected AI provider", example = "sk-********************************" ) String apiKey; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java index f238f57d4b..f473fe6acb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java @@ -17,11 +17,14 @@ package org.thingsboard.server.dao.model.sql; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.Table; import lombok.Getter; import lombok.Setter; import lombok.ToString; import org.hibernate.proxy.HibernateProxy; +import org.thingsboard.server.common.data.ai.AiProvider; import org.thingsboard.server.common.data.ai.AiSettings; import org.thingsboard.server.common.data.id.AiSettingsId; import org.thingsboard.server.common.data.id.TenantId; @@ -44,8 +47,9 @@ public class AiSettingsEntity extends BaseVersionedEntity { @Column(name = ModelConstants.AI_SETTINGS_NAME_COLUMN_NAME, nullable = false) private String name; + @Enumerated(EnumType.STRING) @Column(name = ModelConstants.AI_SETTINGS_PROVIDER_COLUMN_NAME, nullable = false) - private String provider; + private AiProvider provider; @Column(name = ModelConstants.AI_SETTINGS_MODEL_COLUMN_NAME, nullable = false) private String model; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 5682529224..8810e20599 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -48,7 +48,7 @@ import static org.thingsboard.server.dao.service.ConstraintValidator.validateFie type = ComponentType.EXTERNAL, name = "AI", nodeDescription = "Interact with AI", - nodeDetails = "This node makes requests to LLM based on a prompt and a input message and returns a response in a form of output message", + nodeDetails = "This node makes requests to AI based on a prompt and a input message and returns a response in a form of output message", configClazz = TbAiNodeConfiguration.class ) public final class TbAiNode extends TbAbstractExternalNode implements TbNode { From f2075c6c39c58a84f0d32a455ba3cb2733cabe60 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 16 May 2025 20:06:42 +0300 Subject: [PATCH 016/435] AI rule node: add polymorphic JSON config to AI settings --- .../main/data/upgrade/basic/schema_update.sql | 16 +++---- .../server/service/ai/AiServiceImpl.java | 6 +-- .../server/common/data/ai/AiConfig.java | 47 +++++++++++++++++++ .../server/common/data/ai/AiSettings.java | 7 ++- .../common/data/ai/GoogleAiGeminiConfig.java | 40 ++++++++++++++++ .../common/data/ai/MistralAiConfig.java | 40 ++++++++++++++++ .../server/common/data/ai/OpenAiConfig.java | 40 ++++++++++++++++ .../server/common/data/id/AiSettingsId.java | 2 +- .../server/dao/model/ModelConstants.java | 2 +- .../dao/model/sql/AiSettingsEntity.java | 14 ++++-- .../main/resources/sql/schema-entities.sql | 16 +++---- 11 files changed, 200 insertions(+), 30 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/AiConfig.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/GoogleAiGeminiConfig.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/MistralAiConfig.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/OpenAiConfig.java diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index f25ca02939..e148403be2 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -15,13 +15,13 @@ -- CREATE TABLE ai_settings ( - id UUID NOT NULL PRIMARY KEY, - created_time BIGINT NOT NULL, - tenant_id UUID NOT NULL, - version BIGINT NOT NULL DEFAULT 1, - name VARCHAR(255) NOT NULL, - provider VARCHAR(255) NOT NULL, - model VARCHAR(255) NOT NULL, - api_key VARCHAR(1000) NOT NULL, + id UUID NOT NULL PRIMARY KEY, + created_time BIGINT NOT NULL, + tenant_id UUID NOT NULL, + version BIGINT NOT NULL DEFAULT 1, + name VARCHAR(255) NOT NULL, + provider VARCHAR(255) NOT NULL, + model VARCHAR(255) NOT NULL, + configuration JSONB NOT NULL, CONSTRAINT ai_settings_name_unq_key UNIQUE (tenant_id, name) ); diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java index 9306ce5c71..7207501d7e 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java @@ -46,15 +46,15 @@ class AiServiceImpl implements RuleEngineAiService { return switch (aiSettings.getProvider()) { case OPENAI -> OpenAiChatModel.builder() - .apiKey(aiSettings.getApiKey()) + .apiKey(aiSettings.getConfiguration().getApiKey()) .modelName(aiSettings.getModel()) .build(); case MISTRAL_AI -> MistralAiChatModel.builder() - .apiKey(aiSettings.getApiKey()) + .apiKey(aiSettings.getConfiguration().getApiKey()) .modelName(aiSettings.getModel()) .build(); case GOOGLE_AI_GEMINI -> GoogleAiGeminiChatModel.builder() - .apiKey(aiSettings.getApiKey()) + .apiKey(aiSettings.getConfiguration().getApiKey()) .modelName(aiSettings.getModel()) .build(); }; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiConfig.java new file mode 100644 index 0000000000..5c37d21454 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiConfig.java @@ -0,0 +1,47 @@ +/** + * 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; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "provider", + visible = true +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = OpenAiConfig.class, name = "OPENAI"), + @JsonSubTypes.Type(value = GoogleAiGeminiConfig.class, name = "GOOGLE_AI_GEMINI"), + @JsonSubTypes.Type(value = MistralAiConfig.class, name = "MISTRAL_AI") +}) +public abstract class AiConfig { + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_WRITE, + description = "API key for authenticating with the AI provider", + example = "sk-********************************" + ) + private String apiKey; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java index 81a27ae914..22aff4bb62 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java @@ -83,11 +83,10 @@ public final class AiSettings extends BaseData implements HasTenan @Schema( requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.WRITE_ONLY, - description = "API key for authenticating with the selected AI provider", - example = "sk-********************************" + accessMode = Schema.AccessMode.READ_WRITE, + description = "Provider-specific settings for the chosen AI model" ) - String apiKey; + AiConfig configuration; public AiSettings() {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/GoogleAiGeminiConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/GoogleAiGeminiConfig.java new file mode 100644 index 0000000000..915fb41d55 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/GoogleAiGeminiConfig.java @@ -0,0 +1,40 @@ +/** + * 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; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema( + name = "GoogleAiGemini", + description = "Configuration properties for the Google AI Gemini" +) +public class GoogleAiGeminiConfig extends AiConfig { + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_WRITE, + description = "Name of the AI provider", + example = "GOOGLE_AI_GEMINI", + allowableValues = "GOOGLE_AI_GEMINI", + type = "string" + ) + private AiProvider provider = AiProvider.GOOGLE_AI_GEMINI; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/MistralAiConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/MistralAiConfig.java new file mode 100644 index 0000000000..bbfd849f59 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/MistralAiConfig.java @@ -0,0 +1,40 @@ +/** + * 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; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema( + name = "MistralAi", + description = "Configuration properties for the Mistral AI" +) +public class MistralAiConfig extends AiConfig { + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_WRITE, + description = "Name of the AI provider", + example = "MISTRAL_AI", + allowableValues = "MISTRAL_AI", + type = "string" + ) + private AiProvider provider = AiProvider.MISTRAL_AI; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/OpenAiConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/OpenAiConfig.java new file mode 100644 index 0000000000..cce6e3aa90 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/OpenAiConfig.java @@ -0,0 +1,40 @@ +/** + * 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; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema( + name = "OpenAiConfig", + description = "Configuration properties for the OpenAI" +) +public class OpenAiConfig extends AiConfig { + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_WRITE, + description = "Name of the AI provider", + example = "OPENAI", + allowableValues = "OPENAI", + type = "string" + ) + private AiProvider provider = AiProvider.OPENAI; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java index f9b6ab74fe..8aac27051a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java @@ -36,7 +36,7 @@ public final class AiSettingsId extends UUIDBased implements EntityId { @Override @Schema( requiredMode = Schema.RequiredMode.REQUIRED, - description = "Entity type of the AI settings, always 'AI_SETTINGS'", + description = "Entity type of the AI settings", example = "AI_SETTINGS", allowableValues = "AI_SETTINGS" ) diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index ac09ce088b..20ef6ceb08 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -747,7 +747,7 @@ public class ModelConstants { public static final String AI_SETTINGS_NAME_COLUMN_NAME = NAME_PROPERTY; public static final String AI_SETTINGS_PROVIDER_COLUMN_NAME = "provider"; public static final String AI_SETTINGS_MODEL_COLUMN_NAME = "model"; - public static final String AI_SETTINGS_API_KEY_COLUMN_NAME = "api_key"; + public static final String AI_SETTINGS_CONFIGURATION_COLUMN_NAME = "configuration"; protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java index f473fe6acb..458b434711 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.model.sql; +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -23,7 +24,9 @@ import jakarta.persistence.Table; import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.hibernate.annotations.Type; import org.hibernate.proxy.HibernateProxy; +import org.thingsboard.server.common.data.ai.AiConfig; import org.thingsboard.server.common.data.ai.AiProvider; import org.thingsboard.server.common.data.ai.AiSettings; import org.thingsboard.server.common.data.id.AiSettingsId; @@ -41,7 +44,7 @@ import java.util.UUID; @Table(name = ModelConstants.AI_SETTINGS_TABLE_NAME) public class AiSettingsEntity extends BaseVersionedEntity { - @Column(name = ModelConstants.AI_SETTINGS_TENANT_ID_COLUMN_NAME, nullable = false, columnDefinition = "uuid") + @Column(name = ModelConstants.AI_SETTINGS_TENANT_ID_COLUMN_NAME, nullable = false, columnDefinition = "UUID") private UUID tenantId; @Column(name = ModelConstants.AI_SETTINGS_NAME_COLUMN_NAME, nullable = false) @@ -54,8 +57,9 @@ public class AiSettingsEntity extends BaseVersionedEntity { @Column(name = ModelConstants.AI_SETTINGS_MODEL_COLUMN_NAME, nullable = false) private String model; - @Column(name = ModelConstants.AI_SETTINGS_API_KEY_COLUMN_NAME, nullable = false) - private String apiKey; + @Type(JsonBinaryType.class) + @Column(name = ModelConstants.AI_SETTINGS_CONFIGURATION_COLUMN_NAME, nullable = false, columnDefinition = "JSONB") + private AiConfig configuration; public AiSettingsEntity() {} @@ -65,7 +69,7 @@ public class AiSettingsEntity extends BaseVersionedEntity { name = aiSettings.getName(); provider = aiSettings.getProvider(); model = aiSettings.getModel(); - apiKey = aiSettings.getApiKey(); + configuration = aiSettings.getConfiguration(); } @Override @@ -77,7 +81,7 @@ public class AiSettingsEntity extends BaseVersionedEntity { settings.setName(name); settings.setProvider(provider); settings.setModel(model); - settings.setApiKey(apiKey); + settings.setConfiguration(configuration); return settings; } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 9d5ae69c95..b2b9ffaded 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -950,13 +950,13 @@ CREATE TABLE IF NOT EXISTS cf_debug_event ( ) PARTITION BY RANGE (ts); CREATE TABLE IF NOT EXISTS ai_settings ( - id UUID NOT NULL PRIMARY KEY, - created_time BIGINT NOT NULL, - tenant_id UUID NOT NULL, - version BIGINT NOT NULL DEFAULT 1, - name VARCHAR(255) NOT NULL, - provider VARCHAR(255) NOT NULL, - model VARCHAR(255) NOT NULL, - api_key VARCHAR(1000) NOT NULL, + id UUID NOT NULL PRIMARY KEY, + created_time BIGINT NOT NULL, + tenant_id UUID NOT NULL, + version BIGINT NOT NULL DEFAULT 1, + name VARCHAR(255) NOT NULL, + provider VARCHAR(255) NOT NULL, + model VARCHAR(255) NOT NULL, + configuration JSONB NOT NULL, CONSTRAINT ai_settings_name_unq_key UNIQUE (tenant_id, name) ); From 7c94c43d66997f817e7f2ec2b27f806db2ba8a89 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 20 May 2025 16:16:30 +0300 Subject: [PATCH 017/435] AI rule node: refactor configuration field description in AiSettings --- .../java/org/thingsboard/server/common/data/ai/AiSettings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java index 22aff4bb62..d3ef2fa419 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java @@ -84,7 +84,7 @@ public final class AiSettings extends BaseData implements HasTenan @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_WRITE, - description = "Provider-specific settings for the chosen AI model" + description = "Settings specific to the selected AI provider and model" ) AiConfig configuration; From d2c275b761d06ea626fd4d4a0f592ce3a65146d7 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 20 May 2025 16:16:51 +0300 Subject: [PATCH 018/435] AI rule node: correctly call node lifecycle methods --- .../main/java/org/thingsboard/rule/engine/ai/TbAiNode.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 8810e20599..e3e9bdbf61 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -59,6 +59,8 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + super.init(ctx); + var config = TbNodeUtils.convert(configuration, TbAiNodeConfiguration.class); String errorPrefix = "'" + ctx.getSelf().getName() + "' node configuration is invalid: "; try { @@ -66,6 +68,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { } catch (DataValidationException e) { throw new TbNodeException(e, true); } + systemMessage = SystemMessage.from(config.getSystemPrompt()); userPromptTemplate = PromptTemplate.from(""" User-provided task or question: %s @@ -114,6 +117,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { @Override public void destroy() { + super.destroy(); systemMessage = null; userPromptTemplate = null; chatModel = null; From 70930325164e7616b57bc5d5acda0d11c0e22576 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Sat, 10 May 2025 14:24:04 +0200 Subject: [PATCH 019/435] refactored obsolete com.github.java-json-tools:json-schema-validator with actively updated com.networknt:json-schema-validator --- common/cluster-api/pom.xml | 4 - common/dao-api/pom.xml | 4 - dao/pom.xml | 4 + .../BaseComponentDescriptorService.java | 20 ++-- .../BaseComponentDescriptorServiceTest.java | 98 +++++++++++++++++++ pom.xml | 10 +- 6 files changed, 116 insertions(+), 24 deletions(-) create mode 100644 dao/src/test/java/org/thingsboard/server/dao/component/BaseComponentDescriptorServiceTest.java diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index d92b87fbb0..d2ad3b48a8 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -60,10 +60,6 @@ jakarta.annotation jakarta.annotation-api - - com.github.java-json-tools - json-schema-validator - org.slf4j slf4j-api diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index cb8e578760..a4b4d807dd 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -56,10 +56,6 @@ jakarta.annotation jakarta.annotation-api - - com.github.java-json-tools - json-schema-validator - org.slf4j slf4j-api diff --git a/dao/pom.xml b/dao/pom.xml index 9dca643b5d..2206d8a7d7 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -59,6 +59,10 @@ org.thingsboard.common util + + com.networknt + json-schema-validator + org.slf4j slf4j-api diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java index 5acf1b0f97..2c5d6fb3c5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java @@ -16,10 +16,10 @@ package org.thingsboard.server.dao.component; import com.fasterxml.jackson.databind.JsonNode; -import com.github.fge.jsonschema.core.exceptions.ProcessingException; -import com.github.fge.jsonschema.core.report.ProcessingReport; -import com.github.fge.jsonschema.main.JsonSchemaFactory; -import com.github.fge.jsonschema.main.JsonValidator; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SpecVersion; +import com.networknt.schema.ValidationMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -36,6 +36,7 @@ import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.Validator; import java.util.Optional; +import java.util.Set; /** * @author Andrew Shvayka @@ -89,15 +90,18 @@ public class BaseComponentDescriptorService implements ComponentDescriptorServic @Override public boolean validate(TenantId tenantId, ComponentDescriptor component, JsonNode configuration) { - JsonValidator validator = JsonSchemaFactory.byDefault().getValidator(); try { if (!component.getConfigurationDescriptor().has("schema")) { throw new DataValidationException("Configuration descriptor doesn't contain schema property!"); } JsonNode configurationSchema = component.getConfigurationDescriptor().get("schema"); - ProcessingReport report = validator.validate(configurationSchema, configuration); - return report.isSuccess(); - } catch (ProcessingException e) { + + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); + JsonSchema schema = factory.getSchema(configurationSchema); + + Set validationMessages = schema.validate(configuration); + return validationMessages.isEmpty(); + } catch (Exception e) { throw new IncorrectParameterException(e.getMessage(), e); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/component/BaseComponentDescriptorServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/component/BaseComponentDescriptorServiceTest.java new file mode 100644 index 0000000000..d76c11f708 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/component/BaseComponentDescriptorServiceTest.java @@ -0,0 +1,98 @@ +/** + * 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.component; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentClusteringMode; +import org.thingsboard.server.common.data.plugin.ComponentDescriptor; +import org.thingsboard.server.common.data.plugin.ComponentScope; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.dao.exception.IncorrectParameterException; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BaseComponentDescriptorServiceTest { + + private BaseComponentDescriptorService service; + private ComponentDescriptor componentDescriptor; + private TenantId tenantId; + + @BeforeEach + void setUp() { + service = Mockito.spy(BaseComponentDescriptorService.class); + tenantId = TenantId.SYS_TENANT_ID; + + // Create a simple component descriptor + componentDescriptor = new ComponentDescriptor(); + componentDescriptor.setType(ComponentType.ACTION); + componentDescriptor.setScope(ComponentScope.TENANT); + componentDescriptor.setClusteringMode(ComponentClusteringMode.ENABLED); + componentDescriptor.setName("Test Component"); + componentDescriptor.setClazz("org.thingsboard.test.TestComponent"); + + // Create configuration descriptor with schema from JSON string + String configDescriptorJson = """ + { + "schema": { + "type": "object", + "properties": { + "testField": { + "type": "string" + } + }, + "required": ["testField"] + } + }"""; + + componentDescriptor.setConfigurationDescriptor(JacksonUtil.toJsonNode(configDescriptorJson)); + } + + @Test + void testValidate() { + // Create valid configuration from JSON string + String validConfigJson = "{\"testField\": \"test value\"}"; + JsonNode validConfig = JacksonUtil.toJsonNode(validConfigJson); + + // Create invalid configuration (missing required field) from JSON string + String invalidConfigJson = "{}"; + JsonNode invalidConfig = JacksonUtil.toJsonNode(invalidConfigJson); + + // Test valid configuration + boolean validResult = service.validate(tenantId, componentDescriptor, validConfig); + assertTrue(validResult, "Valid configuration should pass validation"); + + // Test invalid configuration + boolean invalidResult = service.validate(tenantId, componentDescriptor, invalidConfig); + assertFalse(invalidResult, "Invalid configuration should fail validation"); + + // Test with component descriptor without schema + ComponentDescriptor noSchemaDescriptor = new ComponentDescriptor(componentDescriptor); + noSchemaDescriptor.setConfigurationDescriptor(JacksonUtil.toJsonNode("{}")); + + // Should throw exception when schema is missing + assertThrows(IncorrectParameterException.class, () -> { + service.validate(tenantId, noSchemaDescriptor, validConfig); + }, "Should throw exception when schema is missing"); + } + +} diff --git a/pom.xml b/pom.xml index 074bbbce03..02d950b830 100755 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ 2.17.2 1.7.0 4.4.0 - 2.2.14 + 1.5.6 0.6.12 3.12.1 2.0.0-M15 @@ -1601,15 +1601,9 @@ ${auth0-jwt.version} - com.github.java-json-tools + com.networknt json-schema-validator ${json-schema-validator.version} - - - com.sun.mail - mailapi - - org.eclipse.leshan From c2776ce7a6371d1ce0262c9e06f6cf3eac14a831 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 20 May 2025 17:59:58 +0300 Subject: [PATCH 020/435] AI rule node: add JSON Schema to rule node config --- common/util/pom.xml | 5 +- .../common/util/JsonSchemaUtils.java | 47 +++++++++++++++++++ .../rule/engine/ai/TbAiNodeConfiguration.java | 20 ++++++-- 3 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 common/util/src/main/java/org/thingsboard/common/util/JsonSchemaUtils.java diff --git a/common/util/pom.xml b/common/util/pom.xml index 6719dc628a..a50cead962 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -114,7 +114,10 @@ net.objecthunter exp4j - ${exp4j.version} + + + com.networknt + json-schema-validator diff --git a/common/util/src/main/java/org/thingsboard/common/util/JsonSchemaUtils.java b/common/util/src/main/java/org/thingsboard/common/util/JsonSchemaUtils.java new file mode 100644 index 0000000000..db45994826 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/JsonSchemaUtils.java @@ -0,0 +1,47 @@ +/** + * 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.common.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SchemaId; +import com.networknt.schema.SchemaLocation; +import com.networknt.schema.SpecVersion; +import com.networknt.schema.ValidationMessage; + +import java.util.Set; + +public final class JsonSchemaUtils { + + private JsonSchemaUtils() { + throw new AssertionError("Can't instantiate utility class"); + } + + /** + * Validates that the provided JsonNode is a valid JSON Schema (Draft 2020-12). + * + * @param schemaNode the JSON Schema document as a JsonNode + * @return true if the schema is well-formed, false otherwise + */ + public static boolean isValidJsonSchema(JsonNode schemaNode) { + Set errors = JsonSchemaFactory + .getInstance(SpecVersion.VersionFlag.V202012) + .getSchema(SchemaLocation.of(SchemaId.V202012)) + .validate(schemaNode); + return errors.isEmpty(); + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index 9d8ac5fbd1..6dd4337aed 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -15,9 +15,12 @@ */ package org.thingsboard.rule.engine.ai; +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; +import org.thingsboard.common.util.JsonSchemaUtils; import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.server.common.data.id.AiSettingsId; import org.thingsboard.server.common.data.validation.Length; @@ -36,15 +39,22 @@ public class TbAiNodeConfiguration implements NodeConfiguration Date: Tue, 20 May 2025 18:51:10 +0300 Subject: [PATCH 021/435] AI rule node: bump langchain4j version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 02d950b830..c68cc9e1e1 100755 --- a/pom.xml +++ b/pom.xml @@ -134,7 +134,7 @@ 1.7.5 3.8.0 2.9.0 - 1.0.0 + 1.0.1 4.2.1 2.7.3 From b64b5795a3b7b0a9dbc6df1f0bbfc23b5dbc1c41 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 21 May 2025 16:03:26 +0300 Subject: [PATCH 022/435] AI rule node: change default prompts --- .../rule/engine/ai/TbAiNodeConfiguration.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index 6dd4337aed..380e5ccd55 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -49,13 +49,8 @@ public class TbAiNodeConfiguration implements NodeConfiguration Date: Wed, 21 May 2025 16:04:31 +0300 Subject: [PATCH 023/435] AI rule node: support text and JSON Schema response formats --- .../ai/Langchain4jJsonSchemaAdapter.java | 135 ++++++++++++++++++ .../thingsboard/rule/engine/ai/TbAiNode.java | 34 ++++- .../rule/engine/ai/TbAiNodeConfiguration.java | 5 + 3 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java new file mode 100644 index 0000000000..9ad745f3ed --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java @@ -0,0 +1,135 @@ +/** + * 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.rule.engine.ai; + +import com.fasterxml.jackson.databind.JsonNode; +import dev.langchain4j.model.chat.request.json.JsonArraySchema; +import dev.langchain4j.model.chat.request.json.JsonBooleanSchema; +import dev.langchain4j.model.chat.request.json.JsonEnumSchema; +import dev.langchain4j.model.chat.request.json.JsonIntegerSchema; +import dev.langchain4j.model.chat.request.json.JsonNullSchema; +import dev.langchain4j.model.chat.request.json.JsonNumberSchema; +import dev.langchain4j.model.chat.request.json.JsonObjectSchema; +import dev.langchain4j.model.chat.request.json.JsonSchema; +import dev.langchain4j.model.chat.request.json.JsonSchemaElement; +import dev.langchain4j.model.chat.request.json.JsonStringSchema; + +import java.util.ArrayList; +import java.util.List; + +/** + * Converts a Jackson {@link JsonNode} JSON Schema into a Langchain4j {@link JsonSchema} model. + */ +final class Langchain4jJsonSchemaAdapter { + + private Langchain4jJsonSchemaAdapter() { + throw new AssertionError("Can't instantiate utility class"); + } + + /** + * Creates a Langchain4j {@link JsonSchema} from the given root JSON Schema node. + * + * @param rootSchemaNode a valid JSON Schema as a Jackson {@link JsonNode} + * @return the corresponding Langchain4j {@link JsonSchema} + */ + public static JsonSchema fromJsonNode(JsonNode rootSchemaNode) { + return JsonSchema.builder() + .name(rootSchemaNode.get("title").textValue()) + .rootElement(parse(rootSchemaNode)) + .build(); + } + + private static JsonSchemaElement parse(JsonNode schemaNode) { + String description = schemaNode.hasNonNull("description") ? schemaNode.get("description").textValue() : null; + + if (schemaNode.has("enum")) { // enum schemas can be defined without 'type' + return parseEnum(schemaNode).description(description).build(); + } + + String type = schemaNode.get("type").textValue(); + + return switch (type) { + case "string" -> JsonStringSchema.builder().description(description).build(); + case "integer" -> JsonIntegerSchema.builder().description(description).build(); + case "boolean" -> JsonBooleanSchema.builder().description(description).build(); + case "number" -> JsonNumberSchema.builder().description(description).build(); + case "null" -> new JsonNullSchema(); + case "object" -> parseObject(schemaNode).description(description).build(); + case "array" -> parseArray(schemaNode).description(description).build(); + default -> throw new IllegalArgumentException("Unsupported JSON Schema type: " + type); + }; + } + + private static JsonEnumSchema.Builder parseEnum(JsonNode enumSchema) { + var builder = new JsonEnumSchema.Builder(); + + List enumValues = new ArrayList<>(); + for (JsonNode element : enumSchema.get("enum")) { + if (!element.isTextual()) { + throw new IllegalArgumentException("Expected each 'enum' element to be a string, but found: " + element.getNodeType()); + } + enumValues.add(element.textValue()); + } + builder.enumValues(enumValues); + + return builder; + } + + private static JsonObjectSchema.Builder parseObject(JsonNode objectSchema) { + var builder = new JsonObjectSchema.Builder(); + + JsonNode propertiesNode = objectSchema.get("properties"); + if (propertiesNode != null) { + propertiesNode.fields().forEachRemaining(entry -> { + String key = entry.getKey(); + JsonNode value = entry.getValue(); + builder.addProperty(key, parse(value)); + }); + } + + List required = new ArrayList<>(); + JsonNode requiredNode = objectSchema.get("required"); + if (requiredNode != null) { + for (JsonNode value : requiredNode) { + required.add(value.textValue()); + } + } + builder.required(required); + + boolean additionalProperties = true; // default value if 'additionalProperties' is not set + JsonNode additionalPropertiesNode = objectSchema.get("additionalProperties"); + if (additionalPropertiesNode != null) { + if (!additionalPropertiesNode.isBoolean()) { + throw new IllegalArgumentException("Expected 'additionalProperties' to be a boolean, but found: " + additionalPropertiesNode.getNodeType()); + } + additionalProperties = additionalPropertiesNode.booleanValue(); + } + builder.additionalProperties(additionalProperties); + + return builder; + } + + private static JsonArraySchema.Builder parseArray(JsonNode arraySchema) { + var builder = new JsonArraySchema.Builder(); + + if (arraySchema.hasNonNull("items")) { + builder.items(parse(arraySchema.get("items"))); + } + + return builder; + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index e3e9bdbf61..315a02a3b9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -15,6 +15,7 @@ */ package org.thingsboard.rule.engine.ai; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.ListenableFuture; import dev.langchain4j.data.message.SystemMessage; @@ -22,6 +23,8 @@ import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.request.ResponseFormat; +import dev.langchain4j.model.chat.request.ResponseFormatType; +import dev.langchain4j.model.chat.request.json.JsonSchema; import dev.langchain4j.model.input.PromptTemplate; import org.checkerframework.checker.nullness.qual.NonNull; import org.thingsboard.common.util.JacksonUtil; @@ -55,6 +58,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { private SystemMessage systemMessage; private PromptTemplate userPromptTemplate; + private ResponseFormat responseFormat; private ChatModel chatModel; @Override @@ -69,6 +73,11 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { throw new TbNodeException(e, true); } + responseFormat = ResponseFormat.builder() + .type(config.getResponseFormatType()) + .jsonSchema(getJsonSchema(config.getResponseFormatType(), config.getJsonSchema())) + .build(); + systemMessage = SystemMessage.from(config.getSystemPrompt()); userPromptTemplate = PromptTemplate.from(""" User-provided task or question: %s @@ -80,6 +89,13 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { chatModel = ctx.getAiService().configureChatModel(ctx.getTenantId(), config.getAiSettingsId()); } + private static JsonSchema getJsonSchema(ResponseFormatType responseFormatType, JsonNode jsonSchema) { + if (responseFormatType == ResponseFormatType.TEXT) { + return null; + } + return responseFormatType == ResponseFormatType.JSON && jsonSchema != null ? Langchain4jJsonSchemaAdapter.fromJsonNode(jsonSchema) : null; + } + @Override public void onMsg(TbContext ctx, TbMsg msg) { var ackedMsg = ackIfNeeded(ctx, msg); @@ -93,12 +109,15 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { var chatRequest = ChatRequest.builder() .messages(List.of(systemMessage, userMessage)) - .responseFormat(ResponseFormat.JSON) + .responseFormat(responseFormat) .build(); addCallback(sendChatRequest(ctx, chatRequest), new FutureCallback<>() { @Override public void onSuccess(String response) { + if (!isValidJson(response)) { + response = wrapInJsonObject(response); + } tellSuccess(ctx, ackedMsg.transform() .data(response) .build()); @@ -115,11 +134,24 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { return ctx.getExternalCallExecutor().submit(() -> chatModel.chat(chatRequest).aiMessage().text()); } + private static boolean isValidJson(String jsonString) { + try { + return JacksonUtil.toJsonNode(jsonString) != null; + } catch (IllegalArgumentException e) { + return false; + } + } + + private static String wrapInJsonObject(String response) { + return JacksonUtil.newObjectNode().put("response", response).toString(); + } + @Override public void destroy() { super.destroy(); systemMessage = null; userPromptTemplate = null; + responseFormat = null; chatModel = null; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index 380e5ccd55..c3234f61bb 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.ai; import com.fasterxml.jackson.databind.JsonNode; +import dev.langchain4j.model.chat.request.ResponseFormatType; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -39,6 +40,9 @@ public class TbAiNodeConfiguration implements NodeConfiguration Date: Thu, 22 May 2025 11:28:21 +0300 Subject: [PATCH 024/435] AI rule node: split AI config into AI provider config and AI model config; add support for model temperature --- .../main/data/upgrade/basic/schema_update.sql | 17 ++++--- .../server/service/ai/AiServiceImpl.java | 47 ++++++++++++----- .../server/common/data/ai/AiSettings.java | 21 ++++++-- .../common/data/ai/model/AiModelConfig.java | 47 +++++++++++++++++ .../model/GoogleAiGeminiChatModelConfig.java | 50 +++++++++++++++++++ .../ai/model/MistralAiChatModelConfig.java | 50 +++++++++++++++++++ .../data/ai/model/OpenAiChatModelConfig.java | 50 +++++++++++++++++++ .../data/ai/{ => provider}/AiProvider.java | 2 +- .../AiProviderConfig.java} | 13 +++-- .../GoogleAiGeminiProviderConfig.java} | 8 +-- .../MistralAiProviderConfig.java} | 8 +-- .../OpenAiProviderConfig.java} | 8 +-- .../server/dao/model/ModelConstants.java | 3 +- .../dao/model/sql/AiSettingsEntity.java | 19 ++++--- .../main/resources/sql/schema-entities.sql | 17 ++++--- 15 files changed, 301 insertions(+), 59 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/GoogleAiGeminiChatModelConfig.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/MistralAiChatModelConfig.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/OpenAiChatModelConfig.java rename common/data/src/main/java/org/thingsboard/server/common/data/ai/{ => provider}/AiProvider.java (92%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/{AiConfig.java => provider/AiProviderConfig.java} (76%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/{GoogleAiGeminiConfig.java => provider/GoogleAiGeminiProviderConfig.java} (82%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/{MistralAiConfig.java => provider/MistralAiProviderConfig.java} (82%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/{OpenAiConfig.java => provider/OpenAiProviderConfig.java} (83%) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index e148403be2..9685847278 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -15,13 +15,14 @@ -- CREATE TABLE ai_settings ( - id UUID NOT NULL PRIMARY KEY, - created_time BIGINT NOT NULL, - tenant_id UUID NOT NULL, - version BIGINT NOT NULL DEFAULT 1, - name VARCHAR(255) NOT NULL, - provider VARCHAR(255) NOT NULL, - model VARCHAR(255) NOT NULL, - configuration JSONB NOT NULL, + id UUID NOT NULL PRIMARY KEY, + created_time BIGINT NOT NULL, + tenant_id UUID NOT NULL, + version BIGINT NOT NULL DEFAULT 1, + name VARCHAR(255) NOT NULL, + provider VARCHAR(255) NOT NULL, + provider_config JSONB NOT NULL, + model VARCHAR(255) NOT NULL, + model_config JSONB, CONSTRAINT ai_settings_name_unq_key UNIQUE (tenant_id, name) ); diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java index 7207501d7e..864c0034c1 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java @@ -23,6 +23,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.RuleEngineAiService; import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.model.MistralAiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.OpenAiChatModelConfig; import org.thingsboard.server.common.data.id.AiSettingsId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.ai.AiSettingsService; @@ -45,18 +47,39 @@ class AiServiceImpl implements RuleEngineAiService { var aiSettings = aiSettingsOpt.get(); return switch (aiSettings.getProvider()) { - case OPENAI -> OpenAiChatModel.builder() - .apiKey(aiSettings.getConfiguration().getApiKey()) - .modelName(aiSettings.getModel()) - .build(); - case MISTRAL_AI -> MistralAiChatModel.builder() - .apiKey(aiSettings.getConfiguration().getApiKey()) - .modelName(aiSettings.getModel()) - .build(); - case GOOGLE_AI_GEMINI -> GoogleAiGeminiChatModel.builder() - .apiKey(aiSettings.getConfiguration().getApiKey()) - .modelName(aiSettings.getModel()) - .build(); + case OPENAI -> { + var modelBuilder = OpenAiChatModel.builder() + .apiKey(aiSettings.getProviderConfig().getApiKey()) + .modelName(aiSettings.getModel()); + + if (aiSettings.getModelConfig() instanceof OpenAiChatModelConfig config) { + modelBuilder.temperature(config.getTemperature()); + } + + yield modelBuilder.build(); + } + case MISTRAL_AI -> { + var modelBuilder = MistralAiChatModel.builder() + .apiKey(aiSettings.getProviderConfig().getApiKey()) + .modelName(aiSettings.getModel()); + + if (aiSettings.getModelConfig() instanceof MistralAiChatModelConfig config) { + modelBuilder.temperature(config.getTemperature()); + } + + yield modelBuilder.build(); + } + case GOOGLE_AI_GEMINI -> { + var modelBuilder = GoogleAiGeminiChatModel.builder() + .apiKey(aiSettings.getProviderConfig().getApiKey()) + .modelName(aiSettings.getModel()); + + if (aiSettings.getModelConfig() instanceof OpenAiChatModelConfig config) { + modelBuilder.temperature(config.getTemperature()); + } + + yield modelBuilder.build(); + } }; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java index d3ef2fa419..6083f09f90 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java @@ -24,6 +24,9 @@ import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasVersion; +import org.thingsboard.server.common.data.ai.model.AiModelConfig; +import org.thingsboard.server.common.data.ai.provider.AiProvider; +import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; import org.thingsboard.server.common.data.id.AiSettingsId; import org.thingsboard.server.common.data.id.TenantId; @@ -76,17 +79,27 @@ public final class AiSettings extends BaseData implements HasTenan @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_WRITE, - description = "Identifier of the AI model to use", + description = "Configuration specific to the AI provider" + ) + AiProviderConfig providerConfig; + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_WRITE, + description = "Identifier of the AI model", example = "gpt-4o-mini" ) String model; @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, + requiredMode = Schema.RequiredMode.NOT_REQUIRED, accessMode = Schema.AccessMode.READ_WRITE, - description = "Settings specific to the selected AI provider and model" + description = """ + Optional configuration specific to the AI model. + If provided, it must be one of the known AiModelConfig subtypes and any settings + you specify will override the model’s defaults; if omitted, the model will run with its built-in defaults.""" ) - AiConfig configuration; + AiModelConfig modelConfig; public AiSettings() {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java new file mode 100644 index 0000000000..5112df3213 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "model", + visible = true +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = OpenAiChatModelConfig.class, name = "gpt-4o"), + @JsonSubTypes.Type(value = OpenAiChatModelConfig.class, name = "gpt-4o-mini"), + @JsonSubTypes.Type(value = GoogleAiGeminiChatModelConfig.class, name = "gemini-2.0-flash"), + @JsonSubTypes.Type(value = MistralAiChatModelConfig.class, name = "mistral-medium-latest") +}) +public abstract class AiModelConfig { + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_WRITE, + description = "Identifier of the AI model" + ) + private String model; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/GoogleAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/GoogleAiGeminiChatModelConfig.java new file mode 100644 index 0000000000..761ec998cc --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/GoogleAiGeminiChatModelConfig.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@Schema( + name = "GoogleAiGeminiChatModelConfig", + description = "Configuration for Google AI Gemini chat models" +) +public final class GoogleAiGeminiChatModelConfig extends AiModelConfig { + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_WRITE, + description = "Identifier of the AI model", + allowableValues = "gemini-2.0-flash", + example = "gemini-2.0-flash" + ) + public String getModel() { + return super.getModel(); + } + + @Schema( + accessMode = Schema.AccessMode.READ_WRITE, + description = "Sampling temperature to control randomness: 0.0 (most deterministic) to 1.0 (most creative)", + example = "0.7" + ) + private Double temperature; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/MistralAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/MistralAiChatModelConfig.java new file mode 100644 index 0000000000..20a8cfea7c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/MistralAiChatModelConfig.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@Schema( + name = "MistralAiChatModelConfig", + description = "Configuration for Mistral AI chat models" +) +public final class MistralAiChatModelConfig extends AiModelConfig { + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_WRITE, + description = "Identifier of the AI model", + allowableValues = "mistral-medium-latest", + example = "mistral-medium-latest" + ) + public String getModel() { + return super.getModel(); + } + + @Schema( + accessMode = Schema.AccessMode.READ_WRITE, + description = "Sampling temperature to control randomness: 0.0 (most deterministic) to 1.0 (most creative)", + example = "0.7" + ) + private Double temperature; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/OpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/OpenAiChatModelConfig.java new file mode 100644 index 0000000000..a7bc3725ef --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/OpenAiChatModelConfig.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@Schema( + name = "OpenAiChatModelConfig", + description = "Configuration for OpenAI chat models" +) +public final class OpenAiChatModelConfig extends AiModelConfig { + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_WRITE, + description = "Identifier of the AI model", + allowableValues = {"gpt-4o", "gpt-4o-mini"}, + example = "gpt-4o" + ) + public String getModel() { + return super.getModel(); + } + + @Schema( + accessMode = Schema.AccessMode.READ_WRITE, + description = "Sampling temperature to control randomness: 0.0 (most deterministic) to 1.0 (most creative)", + example = "0.7" + ) + private Double temperature; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiProvider.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java similarity index 92% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/AiProvider.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java index e2938cdddc..068d75873b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiProvider.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.ai; +package org.thingsboard.server.common.data.ai.provider; public enum AiProvider { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java similarity index 76% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/AiConfig.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java index 5c37d21454..06b82cdef2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.ai; +package org.thingsboard.server.common.data.ai.provider; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -30,17 +30,16 @@ import lombok.NoArgsConstructor; visible = true ) @JsonSubTypes({ - @JsonSubTypes.Type(value = OpenAiConfig.class, name = "OPENAI"), - @JsonSubTypes.Type(value = GoogleAiGeminiConfig.class, name = "GOOGLE_AI_GEMINI"), - @JsonSubTypes.Type(value = MistralAiConfig.class, name = "MISTRAL_AI") + @JsonSubTypes.Type(value = OpenAiProviderConfig.class, name = "OPENAI"), + @JsonSubTypes.Type(value = GoogleAiGeminiProviderConfig.class, name = "GOOGLE_AI_GEMINI"), + @JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI") }) -public abstract class AiConfig { +public abstract class AiProviderConfig { @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_WRITE, - description = "API key for authenticating with the AI provider", - example = "sk-********************************" + description = "API key for authenticating with the AI provider" ) private String apiKey; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/GoogleAiGeminiConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java similarity index 82% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/GoogleAiGeminiConfig.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java index 915fb41d55..9ca8643173 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/GoogleAiGeminiConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.ai; +package org.thingsboard.server.common.data.ai.provider; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -22,10 +22,10 @@ import lombok.EqualsAndHashCode; @Data @EqualsAndHashCode(callSuper = true) @Schema( - name = "GoogleAiGemini", - description = "Configuration properties for the Google AI Gemini" + name = "GoogleAiGeminiProviderConfig", + description = "Configuration for the Google AI Gemini provider" ) -public class GoogleAiGeminiConfig extends AiConfig { +public final class GoogleAiGeminiProviderConfig extends AiProviderConfig { @Schema( requiredMode = Schema.RequiredMode.REQUIRED, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/MistralAiConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java similarity index 82% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/MistralAiConfig.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java index bbfd849f59..bc17f34220 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/MistralAiConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.ai; +package org.thingsboard.server.common.data.ai.provider; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -22,10 +22,10 @@ import lombok.EqualsAndHashCode; @Data @EqualsAndHashCode(callSuper = true) @Schema( - name = "MistralAi", - description = "Configuration properties for the Mistral AI" + name = "MistralAiProviderConfig", + description = "Configuration for the Mistral AI provider" ) -public class MistralAiConfig extends AiConfig { +public final class MistralAiProviderConfig extends AiProviderConfig { @Schema( requiredMode = Schema.RequiredMode.REQUIRED, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/OpenAiConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java similarity index 83% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/OpenAiConfig.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java index cce6e3aa90..36ac8b4b35 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/OpenAiConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.ai; +package org.thingsboard.server.common.data.ai.provider; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -22,10 +22,10 @@ import lombok.EqualsAndHashCode; @Data @EqualsAndHashCode(callSuper = true) @Schema( - name = "OpenAiConfig", - description = "Configuration properties for the OpenAI" + name = "OpenAiProviderConfig", + description = "Configuration for the OpenAI provider" ) -public class OpenAiConfig extends AiConfig { +public final class OpenAiProviderConfig extends AiProviderConfig { @Schema( requiredMode = Schema.RequiredMode.REQUIRED, diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 20ef6ceb08..ecd93df6fd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -746,8 +746,9 @@ public class ModelConstants { public static final String AI_SETTINGS_TENANT_ID_COLUMN_NAME = TENANT_ID_COLUMN; public static final String AI_SETTINGS_NAME_COLUMN_NAME = NAME_PROPERTY; public static final String AI_SETTINGS_PROVIDER_COLUMN_NAME = "provider"; + public static final String AI_SETTINGS_PROVIDER_CONFIG_COLUMN_NAME = "provider_config"; public static final String AI_SETTINGS_MODEL_COLUMN_NAME = "model"; - public static final String AI_SETTINGS_CONFIGURATION_COLUMN_NAME = "configuration"; + public static final String AI_SETTINGS_MODEL_CONFIG_COLUMN_NAME = "model_config"; protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java index 458b434711..e2775e3cbb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java @@ -26,9 +26,10 @@ import lombok.Setter; import lombok.ToString; import org.hibernate.annotations.Type; import org.hibernate.proxy.HibernateProxy; -import org.thingsboard.server.common.data.ai.AiConfig; -import org.thingsboard.server.common.data.ai.AiProvider; import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.model.AiModelConfig; +import org.thingsboard.server.common.data.ai.provider.AiProvider; +import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; import org.thingsboard.server.common.data.id.AiSettingsId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseVersionedEntity; @@ -54,12 +55,16 @@ public class AiSettingsEntity extends BaseVersionedEntity { @Column(name = ModelConstants.AI_SETTINGS_PROVIDER_COLUMN_NAME, nullable = false) private AiProvider provider; + @Type(JsonBinaryType.class) + @Column(name = ModelConstants.AI_SETTINGS_PROVIDER_CONFIG_COLUMN_NAME, nullable = false, columnDefinition = "JSONB") + private AiProviderConfig providerConfig; + @Column(name = ModelConstants.AI_SETTINGS_MODEL_COLUMN_NAME, nullable = false) private String model; @Type(JsonBinaryType.class) - @Column(name = ModelConstants.AI_SETTINGS_CONFIGURATION_COLUMN_NAME, nullable = false, columnDefinition = "JSONB") - private AiConfig configuration; + @Column(name = ModelConstants.AI_SETTINGS_MODEL_CONFIG_COLUMN_NAME, columnDefinition = "JSONB") + private AiModelConfig modelConfig; public AiSettingsEntity() {} @@ -68,8 +73,9 @@ public class AiSettingsEntity extends BaseVersionedEntity { tenantId = getTenantUuid(aiSettings.getTenantId()); name = aiSettings.getName(); provider = aiSettings.getProvider(); + providerConfig = aiSettings.getProviderConfig(); model = aiSettings.getModel(); - configuration = aiSettings.getConfiguration(); + modelConfig = aiSettings.getModelConfig(); } @Override @@ -80,8 +86,9 @@ public class AiSettingsEntity extends BaseVersionedEntity { settings.setTenantId(TenantId.fromUUID(tenantId)); settings.setName(name); settings.setProvider(provider); + settings.setProviderConfig(providerConfig); settings.setModel(model); - settings.setConfiguration(configuration); + settings.setModelConfig(modelConfig); return settings; } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index b2b9ffaded..d2fabc71ec 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -950,13 +950,14 @@ CREATE TABLE IF NOT EXISTS cf_debug_event ( ) PARTITION BY RANGE (ts); CREATE TABLE IF NOT EXISTS ai_settings ( - id UUID NOT NULL PRIMARY KEY, - created_time BIGINT NOT NULL, - tenant_id UUID NOT NULL, - version BIGINT NOT NULL DEFAULT 1, - name VARCHAR(255) NOT NULL, - provider VARCHAR(255) NOT NULL, - model VARCHAR(255) NOT NULL, - configuration JSONB NOT NULL, + id UUID NOT NULL PRIMARY KEY, + created_time BIGINT NOT NULL, + tenant_id UUID NOT NULL, + version BIGINT NOT NULL DEFAULT 1, + name VARCHAR(255) NOT NULL, + provider VARCHAR(255) NOT NULL, + provider_config JSONB NOT NULL, + model VARCHAR(255) NOT NULL, + model_config JSONB, CONSTRAINT ai_settings_name_unq_key UNIQUE (tenant_id, name) ); From 71d1e3640a38fc40c4a565a0e452197818c7cb07 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 22 May 2025 16:41:16 +0300 Subject: [PATCH 025/435] AI rule node: fix type when configuring temperature for Gemini models --- .../java/org/thingsboard/server/service/ai/AiServiceImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java index 864c0034c1..60d7dca7a8 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java @@ -23,6 +23,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.RuleEngineAiService; import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.model.GoogleAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.MistralAiChatModelConfig; import org.thingsboard.server.common.data.ai.model.OpenAiChatModelConfig; import org.thingsboard.server.common.data.id.AiSettingsId; @@ -74,7 +75,7 @@ class AiServiceImpl implements RuleEngineAiService { .apiKey(aiSettings.getProviderConfig().getApiKey()) .modelName(aiSettings.getModel()); - if (aiSettings.getModelConfig() instanceof OpenAiChatModelConfig config) { + if (aiSettings.getModelConfig() instanceof GoogleAiGeminiChatModelConfig config) { modelBuilder.temperature(config.getTemperature()); } From 9111574eca75d1f0a4fa7a1981bb7ad64c8742f5 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 22 May 2025 16:42:11 +0300 Subject: [PATCH 026/435] AI rule node: formatting fix for `modelConfig` field doc --- .../java/org/thingsboard/server/common/data/ai/AiSettings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java index 6083f09f90..d38ac45992 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java @@ -96,7 +96,7 @@ public final class AiSettings extends BaseData implements HasTenan accessMode = Schema.AccessMode.READ_WRITE, description = """ Optional configuration specific to the AI model. - If provided, it must be one of the known AiModelConfig subtypes and any settings + If provided, it must be one of the known `AiModelConfig` subtypes and any settings you specify will override the model’s defaults; if omitted, the model will run with its built-in defaults.""" ) AiModelConfig modelConfig; From fa667a9e661a623cc345b3307a38ede03080c834 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 22 May 2025 16:43:01 +0300 Subject: [PATCH 027/435] AI rule node: add TbAiSettingsService --- .../controller/AiSettingsController.java | 7 +- .../server/controller/BaseController.java | 4 + .../ai/DefaultTbAiSettingsService.java | 79 +++++++++++++++++++ .../entitiy/ai/TbAiSettingsService.java | 27 +++++++ .../src/main/resources/thingsboard.yml | 33 ++++---- .../app/shared/models/entity-type.models.ts | 8 ++ .../assets/locale/locale.constant-en_US.json | 1 + 7 files changed, 140 insertions(+), 19 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiSettingsService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiSettingsService.java diff --git a/application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java index 2f7aa1dd43..530fc1e56a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java @@ -63,9 +63,10 @@ public class AiSettingsController extends BaseController { @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping public AiSettings saveAiSettings(@RequestBody AiSettings aiSettings) throws ThingsboardException { - aiSettings.setTenantId(getTenantId()); + var user = getCurrentUser(); + aiSettings.setTenantId(user.getTenantId()); checkEntity(aiSettings.getId(), aiSettings, Resource.AI_SETTINGS); - return aiSettingsService.save(aiSettings); + return tbAiSettingsService.save(aiSettings, user); } @ApiOperation( @@ -143,7 +144,7 @@ public class AiSettingsController extends BaseController { return false; } accessControlService.checkPermission(user, Resource.AI_SETTINGS, Operation.DELETE, aiSettingsId, aiSettingsOpt.get()); - return aiSettingsService.deleteByTenantIdAndId(user.getTenantId(), aiSettingsId); + return tbAiSettingsService.delete(aiSettingsOpt.get(), user); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index ef50a6552c..26ff7c1a94 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -177,6 +177,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.entitiy.TbLogEntityActionService; +import org.thingsboard.server.service.entitiy.ai.TbAiSettingsService; import org.thingsboard.server.service.entitiy.user.TbUserSettingsService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; @@ -379,6 +380,9 @@ public abstract class BaseController { @Autowired protected AiSettingsService aiSettingsService; + @Autowired + protected TbAiSettingsService tbAiSettingsService; + @Value("${server.log_controller_error_stack_trace}") @Getter private boolean logControllerErrorStackTrace; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiSettingsService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiSettingsService.java new file mode 100644 index 0000000000..08e663633d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiSettingsService.java @@ -0,0 +1,79 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.entitiy.ai; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.dao.ai.AiSettingsService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; + +import static java.util.Objects.requireNonNullElseGet; + +@Service +@TbCoreComponent +@RequiredArgsConstructor +class DefaultTbAiSettingsService extends AbstractTbEntityService implements TbAiSettingsService { + + private final AiSettingsService aiSettingsService; + + @Override + public AiSettings save(AiSettings aiSettings, User user) { + var actionType = aiSettings.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + + var tenantId = user.getTenantId(); + aiSettings.setTenantId(tenantId); + + AiSettings savedSettings; + try { + savedSettings = aiSettingsService.save(aiSettings); + } catch (Exception e) { + logEntityActionService.logEntityAction(tenantId, requireNonNullElseGet(aiSettings.getId(), () -> emptyId(EntityType.AI_SETTINGS)), aiSettings, actionType, user, e); + throw e; + } + + logEntityActionService.logEntityAction(tenantId, savedSettings.getId(), savedSettings, actionType, user); + + return savedSettings; + } + + @Override + public boolean delete(AiSettings aiSettings, User user) { + var actionType = ActionType.DELETED; + + var tenantId = user.getTenantId(); + var aiSettingsId = aiSettings.getId(); + + boolean deleted; + try { + deleted = aiSettingsService.deleteByTenantIdAndId(tenantId, aiSettingsId); + } catch (Exception e) { + logEntityActionService.logEntityAction(tenantId, aiSettingsId, aiSettings, actionType, user, e, aiSettingsId.toString()); + throw e; + } + + if (deleted) { + logEntityActionService.logEntityAction(tenantId, aiSettingsId, aiSettings, actionType, user, aiSettingsId.toString()); + } + + return deleted; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiSettingsService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiSettingsService.java new file mode 100644 index 0000000000..399a3b2bdd --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiSettingsService.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.entitiy.ai; + +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.ai.AiSettings; + +public interface TbAiSettingsService { + + AiSettings save(AiSettings aiSettings, User user); + + boolean delete(AiSettings aiSettings, User user); + +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index d7dbc0d6ac..5104f2e264 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -846,22 +846,23 @@ audit-log: # Allowed values: OFF (disable), W (log write operations), RW (log read and write operations) logging-level: mask: - "device": "${AUDIT_LOG_MASK_DEVICE:W}" # Device logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "asset": "${AUDIT_LOG_MASK_ASSET:W}" # Asset logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "dashboard": "${AUDIT_LOG_MASK_DASHBOARD:W}" # Dashboard logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "widget_type": "${AUDIT_LOG_MASK_WIDGET_TYPE:W}" # Widget type logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "widgets_bundle": "${AUDIT_LOG_MASK_WIDGETS_BUNDLE:W}" # Widget bundles logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "customer": "${AUDIT_LOG_MASK_CUSTOMER:W}" # Customer logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "user": "${AUDIT_LOG_MASK_USER:W}" # User logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "rule_chain": "${AUDIT_LOG_MASK_RULE_CHAIN:W}" # Rule chain logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "alarm": "${AUDIT_LOG_MASK_ALARM:W}" # Alarm logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "entity_view": "${AUDIT_LOG_MASK_ENTITY_VIEW:W}" # Entity view logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "device_profile": "${AUDIT_LOG_MASK_DEVICE_PROFILE:W}" # Device profile logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "asset_profile": "${AUDIT_LOG_MASK_ASSET_PROFILE:W}" # Asset profile logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "edge": "${AUDIT_LOG_MASK_EDGE:W}" # Edge logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "calculated_field": "${AUDIT_LOG_MASK_CALCULATED_FIELD:W}" # Calculated field logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation + "device": "${AUDIT_LOG_MASK_DEVICE:W}" # Device logging levels. + "asset": "${AUDIT_LOG_MASK_ASSET:W}" # Asset logging levels. + "dashboard": "${AUDIT_LOG_MASK_DASHBOARD:W}" # Dashboard logging levels. + "widget_type": "${AUDIT_LOG_MASK_WIDGET_TYPE:W}" # Widget type logging levels. + "widgets_bundle": "${AUDIT_LOG_MASK_WIDGETS_BUNDLE:W}" # Widget bundles logging levels. + "customer": "${AUDIT_LOG_MASK_CUSTOMER:W}" # Customer logging levels. + "user": "${AUDIT_LOG_MASK_USER:W}" # User logging levels. + "rule_chain": "${AUDIT_LOG_MASK_RULE_CHAIN:W}" # Rule chain logging levels. + "alarm": "${AUDIT_LOG_MASK_ALARM:W}" # Alarm logging levels. + "entity_view": "${AUDIT_LOG_MASK_ENTITY_VIEW:W}" # Entity view logging levels. + "device_profile": "${AUDIT_LOG_MASK_DEVICE_PROFILE:W}" # Device profile logging levels. + "asset_profile": "${AUDIT_LOG_MASK_ASSET_PROFILE:W}" # Asset profile logging levels. + "edge": "${AUDIT_LOG_MASK_EDGE:W}" # Edge logging levels. + "tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels. + "ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package logging levels. + "calculated_field": "${AUDIT_LOG_MASK_CALCULATED_FIELD:W}" # Calculated field logging levels. + "ai_settings": "${AUDIT_LOG_MASK_AI_SETTINGS:W}" # AI settings logging levels. sink: # Type of external sink. possible options: none, elasticsearch type: "${AUDIT_LOG_SINK_TYPE:none}" diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 48cad42e7b..0c7b6ca152 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -51,6 +51,7 @@ export enum EntityType { MOBILE_APP_BUNDLE = 'MOBILE_APP_BUNDLE', MOBILE_APP = 'MOBILE_APP', CALCULATED_FIELD = 'CALCULATED_FIELD', + AI_SETTINGS = 'AI_SETTINGS', } export enum AliasEntityType { @@ -491,6 +492,13 @@ export const entityTypeTranslations = new Map Date: Thu, 22 May 2025 17:37:12 +0300 Subject: [PATCH 028/435] AI rule node: delete AI settings on tenant deletion --- .../org/thingsboard/server/dao/tenant/TenantServiceImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 8c40ca3e14..505fe2a50a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -182,7 +182,8 @@ public class TenantServiceImpl extends AbstractCachedEntityService Date: Thu, 22 May 2025 19:02:19 +0300 Subject: [PATCH 029/435] AI rule node: implement data validator for AI settings --- .../data/ai/provider/AiProviderConfig.java | 2 + .../server/dao/ai/AiSettingsServiceImpl.java | 4 + .../validator/AiSettingsDataValidator.java | 109 ++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/service/validator/AiSettingsDataValidator.java diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java index 06b82cdef2..f350ed75ec 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java @@ -36,6 +36,8 @@ import lombok.NoArgsConstructor; }) public abstract class AiProviderConfig { + public abstract AiProvider getProvider(); + @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_WRITE, diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java index ca23b6bc56..24571458ec 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java @@ -25,6 +25,7 @@ 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.service.DataValidator; import java.util.Optional; @@ -37,8 +38,11 @@ class AiSettingsServiceImpl implements AiSettingsService { private final AiSettingsDao aiSettingsDao; + private final DataValidator aiSettingsValidator; + @Override public AiSettings save(AiSettings aiSettings) { + aiSettingsValidator.validate(aiSettings, AiSettings::getTenantId); try { return aiSettingsDao.saveAndFlush(aiSettings.getTenantId(), aiSettings); } catch (Exception e) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiSettingsDataValidator.java new file mode 100644 index 0000000000..ecfe9ad92a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiSettingsDataValidator.java @@ -0,0 +1,109 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.ai.AiSettingsDao; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.tenant.TenantService; + +import java.util.Objects; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +class AiSettingsDataValidator extends DataValidator { + + private final TenantService tenantService; + private final AiSettingsDao aiSettingsDao; + + @Override + protected void validateCreate(TenantId tenantId, AiSettings aiSettings) { + validateNumberOfEntitiesPerTenant(tenantId, EntityType.AI_SETTINGS); + } + + @Override + protected AiSettings validateUpdate(TenantId tenantId, AiSettings aiSettings) { + Optional old = aiSettingsDao.findByTenantIdAndId(tenantId, aiSettings.getId()); + if (old.isEmpty()) { + throw new DataValidationException("Can't update non existing AI settings!"); + } + return old.get(); + } + + @Override + protected void validateDataImpl(TenantId tenantId, AiSettings aiSettings) { + // ID validation + if (aiSettings.getId() != null) { + if (aiSettings.getUuidId() == null) { + throw new DataValidationException("AI settings UUID should be specified!"); + } + if (aiSettings.getId().isNullUid()) { + throw new DataValidationException("AI settings UUID must not be the reserved null value!"); + } + } + + // tenant ID validation + if (aiSettings.getTenantId() == null || aiSettings.getTenantId().getId() == null) { + throw new DataValidationException("AI settings should be assigned to tenant!"); + } + if (aiSettings.getTenantId().isSysTenantId()) { + throw new DataValidationException("AI settings cannot be assigned to the system tenant!"); + } + if (!tenantService.tenantExists(tenantId)) { + throw new DataValidationException("AI settings reference a non-existent tenant!"); + } + + // name validation + validateString("AI settings name", aiSettings.getName()); + if (aiSettings.getName().length() > 255) { + throw new DataValidationException("AI settings name should be between 1 and 255 symbols!"); + } + + // provider validation + if (aiSettings.getProvider() == null) { + throw new DataValidationException("AI provider should be specified!"); + } + + // provider config validation + if (aiSettings.getProviderConfig() == null) { + throw new DataValidationException("AI provider config should be specified!"); + } + if (aiSettings.getProviderConfig().getProvider() != aiSettings.getProvider()) { + throw new DataValidationException("AI provider configuration should match the selected AI provider!"); + } + validateString("AI provider API key", aiSettings.getProviderConfig().getApiKey()); + + // model identifier validation + validateString("AI model identifier", aiSettings.getModel()); + if (aiSettings.getModel().length() > 255) { + throw new DataValidationException("AI model identifier should be between 1 and 255 symbols!"); + } + + // model config validation + if (aiSettings.getModelConfig() != null) { + if (!Objects.equals(aiSettings.getModelConfig().getModel(), aiSettings.getModel())) { + throw new DataValidationException("AI model configuration should match the selected AI model!"); + } + } + } + +} From 255ab3b39131028fcbd517cacdf64ac2b3a9770e Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 23 May 2025 12:52:01 +0300 Subject: [PATCH 030/435] AI rule node: minor correction to AI settings data validation message --- .../server/dao/service/validator/AiSettingsDataValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiSettingsDataValidator.java index ecfe9ad92a..24d4df23ee 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiSettingsDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiSettingsDataValidator.java @@ -44,7 +44,7 @@ class AiSettingsDataValidator extends DataValidator { protected AiSettings validateUpdate(TenantId tenantId, AiSettings aiSettings) { Optional old = aiSettingsDao.findByTenantIdAndId(tenantId, aiSettings.getId()); if (old.isEmpty()) { - throw new DataValidationException("Can't update non existing AI settings!"); + throw new DataValidationException("Cannot update non-existent AI settings!"); } return old.get(); } From b902c0029da8d0c19607b9ba6891e76547e6ca68 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 23 May 2025 14:47:08 +0300 Subject: [PATCH 031/435] AI rule node: publish save/delete events for AI settings --- .../server/dao/ai/AiSettingsDao.java | 4 +- .../server/dao/ai/AiSettingsServiceImpl.java | 50 ++++++++++++++++--- .../dao/sql/ai/AiSettingsRepository.java | 12 +++-- .../server/dao/sql/ai/JpaAiSettingsDao.java | 12 +++-- 4 files changed, 65 insertions(+), 13 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsDao.java index a94131a642..94a096d679 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsDao.java @@ -27,7 +27,9 @@ public interface AiSettingsDao extends Dao, TenantEntityDao findByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId); - void deleteByTenantId(TenantId tenantId); + boolean deleteById(TenantId tenantId, AiSettingsId aiSettingsId); + + int deleteByTenantId(TenantId tenantId); boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java index 24571458ec..281efbefc1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java @@ -16,7 +16,9 @@ package org.thingsboard.server.dao.ai; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ai.AiSettings; import org.thingsboard.server.common.data.id.AiSettingsId; @@ -25,8 +27,11 @@ 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.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.service.DataValidator; +import java.util.List; import java.util.Optional; import static org.thingsboard.server.dao.entity.AbstractEntityService.checkConstraintViolation; @@ -36,19 +41,33 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink; @RequiredArgsConstructor class AiSettingsServiceImpl implements AiSettingsService { - private final AiSettingsDao aiSettingsDao; - + private final ApplicationEventPublisher eventPublisher; private final DataValidator aiSettingsValidator; + private final AiSettingsDao aiSettingsDao; + @Override public AiSettings save(AiSettings aiSettings) { - aiSettingsValidator.validate(aiSettings, AiSettings::getTenantId); + AiSettings oldSettings = aiSettingsValidator.validate(aiSettings, AiSettings::getTenantId); + + AiSettings savedSettings; try { - return aiSettingsDao.saveAndFlush(aiSettings.getTenantId(), aiSettings); + savedSettings = aiSettingsDao.saveAndFlush(aiSettings.getTenantId(), aiSettings); } catch (Exception e) { checkConstraintViolation(e, "ai_settings_name_unq_key", "AI settings record with such name already exists!"); throw e; } + + eventPublisher.publishEvent(SaveEntityEvent.builder() + .tenantId(savedSettings.getTenantId()) + .entity(savedSettings) + .oldEntity(oldSettings) + .entityId(savedSettings.getId()) + .created(oldSettings == null) + .broadcastEvent(true) + .build()); + + return savedSettings; } @Override @@ -69,7 +88,15 @@ class AiSettingsServiceImpl implements AiSettingsService { @Override public boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { - return aiSettingsDao.deleteByTenantIdAndId(tenantId, aiSettingsId); + Optional aiSettingsOpt = aiSettingsDao.findByTenantIdAndId(tenantId, aiSettingsId); + if (aiSettingsOpt.isEmpty()) { + return false; + } + boolean deleted = aiSettingsDao.deleteByTenantIdAndId(tenantId, aiSettingsId); + if (deleted) { + publishDeleteEvent(aiSettingsOpt.get()); + } + return deleted; } @Override @@ -84,12 +111,23 @@ class AiSettingsServiceImpl implements AiSettingsService { @Override public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { - aiSettingsDao.removeById(tenantId, id.getId()); + deleteByTenantIdAndId(tenantId, new AiSettingsId(id.getId())); } @Override + @Transactional public void deleteByTenantId(TenantId tenantId) { + List deletedSettings = aiSettingsDao.findAllByTenantId(tenantId, new PageLink(Integer.MAX_VALUE)).getData(); aiSettingsDao.deleteByTenantId(tenantId); + deletedSettings.forEach(this::publishDeleteEvent); + } + + private void publishDeleteEvent(AiSettings settings) { + eventPublisher.publishEvent(DeleteEntityEvent.builder() + .tenantId(settings.getTenantId()) + .entityId(settings.getId()) + .entity(settings) + .build()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java index e52177d247..691709f8b8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java @@ -25,6 +25,7 @@ import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.model.sql.AiSettingsEntity; import java.util.Optional; +import java.util.Set; import java.util.UUID; public interface AiSettingsRepository extends JpaRepository { @@ -40,11 +41,16 @@ public interface AiSettingsRepository extends JpaRepository ids); + + @Transactional + int deleteByTenantId(UUID tenantId); @Transactional @Modifying - @Query("DELETE FROM AiSettingsEntity ai WHERE ai.tenantId = :tenantId AND ai.id = :id") - int deleteByTenantIdAndId(@Param("tenantId") UUID tenantId, @Param("id") UUID id); + @Query("DELETE FROM AiSettingsEntity ai WHERE ai.tenantId = :tenantId AND ai.id IN (:ids)") + int deleteByTenantIdAndIdIn(@Param("tenantId") UUID tenantId, @Param("ids") Set ids); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java index 14e43cb095..c56c29b835 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java @@ -32,6 +32,7 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.Optional; +import java.util.Set; import java.util.UUID; @SqlDao @@ -59,13 +60,18 @@ class JpaAiSettingsDao extends JpaAbstractDao impl } @Override - public void deleteByTenantId(TenantId tenantId) { - aiSettingsRepository.deleteByTenantId(tenantId.getId()); + public boolean deleteById(TenantId tenantId, AiSettingsId aiSettingsId) { + return aiSettingsRepository.deleteByIdIn(Set.of(aiSettingsId.getId())) > 0; + } + + @Override + public int deleteByTenantId(TenantId tenantId) { + return aiSettingsRepository.deleteByTenantId(tenantId.getId()); } @Override public boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { - return aiSettingsRepository.deleteByTenantIdAndId(tenantId.getId(), aiSettingsId.getId()) > 0; + return aiSettingsRepository.deleteByTenantIdAndIdIn(tenantId.getId(), Set.of(aiSettingsId.getId())) > 0; } @Override From c738d3a906cfe1627d9c9befad688fa1068b80a5 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 23 May 2025 14:48:47 +0300 Subject: [PATCH 032/435] AI rule node: return early optimization in `deleteByTenantId()` --- .../org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java index 281efbefc1..1bfa316c73 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java @@ -118,6 +118,9 @@ class AiSettingsServiceImpl implements AiSettingsService { @Transactional public void deleteByTenantId(TenantId tenantId) { List deletedSettings = aiSettingsDao.findAllByTenantId(tenantId, new PageLink(Integer.MAX_VALUE)).getData(); + if (deletedSettings.isEmpty()) { + return; + } aiSettingsDao.deleteByTenantId(tenantId); deletedSettings.forEach(this::publishDeleteEvent); } From bdb657e213e41d2a73342a22da00f3b0c42f610f Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 23 May 2025 18:14:38 +0300 Subject: [PATCH 033/435] AI rule node: add AI settings lifecycle events handling in listeners --- .../edge/EdgeEventSourcingListener.java | 4 ++-- .../edge/RelatedEdgesSourcingListener.java | 8 ++++--- .../entitiy/EntityStateSourcingListener.java | 4 ++-- .../queue/DefaultTbClusterService.java | 22 ++++++++++--------- .../dao/housekeeper/CleanUpService.java | 2 +- 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index 7bafd53644..dc3d1a74f7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -112,7 +112,7 @@ public class EdgeEventSourcingListener { return; } try { - if (EntityType.TENANT.equals(entityType) || EntityType.EDGE.equals(entityType)) { + if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_SETTINGS == entityType) { return; } log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event); @@ -226,7 +226,7 @@ public class EdgeEventSourcingListener { break; case TENANT: return !event.getCreated(); - case API_USAGE_STATE, EDGE: + case API_USAGE_STATE, EDGE, AI_SETTINGS: return false; case DOMAIN: if (entity instanceof Domain domain) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java index 61405e17e1..e0c24a13bc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java @@ -58,8 +58,7 @@ public class RelatedEdgesSourcingListener { log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event); try { switch (event.getActionType()) { - case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE -> - relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId()); + case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE -> relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId()); } } catch (Exception e) { log.error("[{}] failed to process ActionEntityEvent: {}", event.getTenantId(), event, e); @@ -67,7 +66,10 @@ public class RelatedEdgesSourcingListener { }); } - @TransactionalEventListener(fallbackExecution = true) + @TransactionalEventListener( + fallbackExecution = true, + condition = "#event.entityId.getEntityType() != T(org.thingsboard.server.common.data.EntityType).AI_SETTINGS" + ) public void handleEvent(DeleteEntityEvent event) { executorService.submit(() -> { log.trace("[{}] DeleteEntityEvent called: {}", event.getTenantId(), event); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index 648e89adc9..d0d9376f12 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -96,7 +96,7 @@ public class EntityStateSourcingListener { case ASSET -> { onAssetUpdate(event.getEntity(), event.getOldEntity()); } - case ASSET_PROFILE, ENTITY_VIEW, NOTIFICATION_RULE -> { + case ASSET_PROFILE, ENTITY_VIEW, NOTIFICATION_RULE, AI_SETTINGS -> { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, lifecycleEvent); } case RULE_CHAIN -> { @@ -158,7 +158,7 @@ public class EntityStateSourcingListener { Asset asset = (Asset) event.getEntity(); tbClusterService.onAssetDeleted(tenantId, asset, null); } - case ASSET_PROFILE, ENTITY_VIEW, CUSTOMER, EDGE, NOTIFICATION_RULE -> { + case ASSET_PROFILE, ENTITY_VIEW, CUSTOMER, EDGE, NOTIFICATION_RULE, AI_SETTINGS -> { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, ComponentLifecycleEvent.DELETED); } case NOTIFICATION_REQUEST -> { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 561dc7122a..aa27b4592b 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -584,16 +584,18 @@ public class DefaultTbClusterService implements TbClusterService { TbQueueProducer> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer(); Set tbRuleEngineServices = partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE); EntityType entityType = msg.getEntityId().getEntityType(); - if (entityType.equals(EntityType.TENANT) - || entityType.equals(EntityType.TENANT_PROFILE) - || entityType.equals(EntityType.DEVICE_PROFILE) - || (entityType.equals(EntityType.ASSET) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) - || entityType.equals(EntityType.ASSET_PROFILE) - || entityType.equals(EntityType.API_USAGE_STATE) - || (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) - || entityType.equals(EntityType.ENTITY_VIEW) - || entityType.equals(EntityType.NOTIFICATION_RULE) - || entityType.equals(EntityType.CALCULATED_FIELD) + if (entityType.isOneOf( + EntityType.TENANT, + EntityType.API_USAGE_STATE, + EntityType.ENTITY_VIEW, + EntityType.NOTIFICATION_RULE, + EntityType.CALCULATED_FIELD, + EntityType.AI_SETTINGS, + EntityType.TENANT_PROFILE, + EntityType.DEVICE_PROFILE, + EntityType.ASSET_PROFILE) + || (entityType == EntityType.ASSET && msg.getEvent() == ComponentLifecycleEvent.UPDATED) + || (entityType == EntityType.DEVICE && msg.getEvent() == ComponentLifecycleEvent.UPDATED) ) { TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java index 1ca2973936..703d944e3b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java @@ -46,7 +46,7 @@ public class CleanUpService { private final Set 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.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, EntityType.AI_SETTINGS ); @TransactionalEventListener(fallbackExecution = true) // after transaction commit From 2ffec818125bf16e7fd736ea901272c3b7edb5b2 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Mon, 2 Jun 2025 12:28:44 +0300 Subject: [PATCH 034/435] CalculatedField functionality support for Edge - added CalculatedField functionality for Edge --- .../service/edge/EdgeContextComponent.java | 8 ++ .../service/edge/EdgeMsgConstructorUtils.java | 16 +++ .../service/edge/rpc/EdgeGrpcSession.java | 7 +- .../service/edge/rpc/EdgeSyncCursor.java | 3 + .../CalculatedFieldsEdgeEventFetcher.java | 48 +++++++ .../BaseCalculatedFieldProcessor.java | 79 +++++++++++ .../CalculatedFieldEdgeProcessor.java | 133 ++++++++++++++++++ .../calculated/CalculatedFieldProcessor.java | 28 ++++ .../server/dao/cf/CalculatedFieldService.java | 4 + .../common/data/edge/EdgeEventType.java | 9 +- .../common/data/id/EntityIdFactory.java | 2 + common/edge-api/src/main/proto/edge.proto | 10 ++ .../dao/cf/BaseCalculatedFieldService.java | 23 ++- .../server/dao/cf/CalculatedFieldDao.java | 3 + .../dao/sql/cf/CalculatedFieldRepository.java | 2 + .../dao/sql/cf/JpaCalculatedFieldDao.java | 7 + 16 files changed, 375 insertions(+), 7 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CalculatedFieldsEdgeEventFetcher.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldProcessor.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index 00ae4a45bd..abdc89ebd9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -29,6 +29,7 @@ import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; @@ -61,6 +62,7 @@ import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmProcessor; import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor; import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.asset.profile.AssetProfileEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.calculated.CalculatedFieldProcessor; import org.thingsboard.server.service.edge.rpc.processor.dashboard.DashboardEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.device.DeviceEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.device.profile.DeviceProfileEdgeProcessor; @@ -248,6 +250,12 @@ public class EdgeContextComponent { @Autowired private GrpcCallbackExecutorService grpcCallbackExecutorService; + @Autowired + private CalculatedFieldService calculatedFieldService; + + @Autowired + private CalculatedFieldProcessor calculatedFieldProcessor; + public EdgeProcessor getProcessor(EdgeEventType edgeEventType) { EdgeProcessor processor = processorMap.get(edgeEventType); if (processor == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 5f5fd771cf..192c56692d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -48,11 +48,13 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.domain.DomainInfo; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; @@ -89,6 +91,7 @@ import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; @@ -638,4 +641,17 @@ public class EdgeMsgConstructorUtils { .build(); } + public static CalculatedFieldUpdateMsg constructCalculatedFieldUpdatedMsg(UpdateMsgType msgType, CalculatedField calculatedField) { + return CalculatedFieldUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(calculatedField)) + .setIdMSB(calculatedField.getId().getId().getMostSignificantBits()) + .setIdLSB(calculatedField.getId().getId().getLeastSignificantBits()).build(); + } + + public static CalculatedFieldUpdateMsg constructCalculatedFieldDeleteMsg(CalculatedFieldId calculatedFieldId) { + return CalculatedFieldUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(calculatedFieldId.getId().getMostSignificantBits()) + .setIdLSB(calculatedFieldId.getId().getLeastSignificantBits()).build(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 4a9b68fc6d..f3335d6d11 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -42,7 +42,6 @@ import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger; 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.common.data.page.TimePageLink; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; @@ -50,6 +49,7 @@ import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg; import org.thingsboard.server.gen.edge.v1.ConnectResponseCode; import org.thingsboard.server.gen.edge.v1.ConnectResponseMsg; @@ -907,6 +907,11 @@ public abstract class EdgeGrpcSession implements Closeable { result.add(ctx.getEdgeRequestsService().processEntityViewsRequestMsg(edge.getTenantId(), edge, entityViewRequestMsg)); } } + if (uplinkMsg.getCalculatedFieldUpdateMsgCount() > 0) { + for (CalculatedFieldUpdateMsg calculatedFieldUpdateMsg : uplinkMsg.getCalculatedFieldUpdateMsgList()) { + result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg)); + } + } } catch (Exception e) { String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); log.trace("[{}][{}] Can't process uplink msg [{}]", edge.getTenantId(), sessionId, uplinkMsg, e); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index 351c9b411b..389f0202fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.edge.rpc.fetch.AdminSettingsEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.AssetProfilesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.AssetsEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.CalculatedFieldsEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.CustomerEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.CustomerUsersEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.DashboardsEdgeEventFetcher; @@ -80,6 +81,7 @@ public class EdgeSyncCursor { fetchers.add(new DevicesEdgeEventFetcher(ctx.getDeviceService())); fetchers.add(new AssetsEdgeEventFetcher(ctx.getAssetService())); fetchers.add(new EntityViewsEdgeEventFetcher(ctx.getEntityViewService())); + fetchers.add(new CalculatedFieldsEdgeEventFetcher(ctx.getCalculatedFieldService())); if (fullSync) { fetchers.add(new NotificationTemplateEdgeEventFetcher(ctx.getNotificationTemplateService())); fetchers.add(new NotificationTargetEdgeEventFetcher(ctx.getNotificationTargetService())); @@ -107,4 +109,5 @@ public class EdgeSyncCursor { currentIdx++; return edgeEventFetcher; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CalculatedFieldsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CalculatedFieldsEdgeEventFetcher.java new file mode 100644 index 0000000000..4f3e91354e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CalculatedFieldsEdgeEventFetcher.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.fetch; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +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.cf.CalculatedFieldService; + +@AllArgsConstructor +@Slf4j +public class CalculatedFieldsEdgeEventFetcher extends BasePageableEdgeEventFetcher { + + private final CalculatedFieldService calculatedFieldService; + + @Override + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { + return calculatedFieldService.findCalculatedFieldsByTenantId(tenantId, pageLink); + } + + @Override + EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, CalculatedField calculatedField) { + return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.CALCULATED_FIELD, + EdgeEventActionType.ADDED, calculatedField.getId(), null); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java new file mode 100644 index 0000000000..ddb1b23d53 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java @@ -0,0 +1,79 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.calculated; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.util.Pair; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Slf4j +public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { + + @Autowired + private DataValidator calculatedFieldValidator; + + protected Pair saveOrUpdateCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg) { + boolean isCreated = false; + boolean isNameUpdated = false; + try { + CalculatedField calculatedField = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); + if (calculatedField == null) { + throw new RuntimeException("[{" + tenantId + "}] calculatedFieldUpdateMsg {" + calculatedFieldUpdateMsg + " } cannot be converted to calculatedField"); + } + + CalculatedField calculatedFieldById = edgeCtx.getCalculatedFieldService().findById(tenantId, calculatedFieldId); + if (calculatedFieldById == null) { + calculatedField.setCreatedTime(Uuids.unixTimestamp(calculatedFieldId.getId())); + isCreated = true; + calculatedField.setId(null); + } else { + calculatedField.setId(calculatedFieldId); + } + + String calculatedFieldName = calculatedField.getName(); + CalculatedField calculatedFieldByName = edgeCtx.getCalculatedFieldService().findByTenantIdAndName(tenantId, calculatedFieldName); + if (calculatedFieldByName != null && !calculatedFieldByName.getId().equals(calculatedFieldId)) { + calculatedFieldName = calculatedFieldName + "_" + StringUtils.randomAlphabetic(15); + log.warn("[{}] calculatedField with name {} already exists. Renaming calculatedField name to {}", + tenantId, calculatedField.getName(), calculatedFieldByName.getName()); + isNameUpdated = true; + } + calculatedField.setName(calculatedFieldName); + + calculatedFieldValidator.validate(calculatedField, CalculatedField::getTenantId); + + if (isCreated) { + calculatedField.setId(calculatedFieldId); + } + + edgeCtx.getCalculatedFieldService().save(calculatedField, false); + } catch (Exception e) { + log.error("[{}] Failed to process calculatedField update msg [{}]", tenantId, calculatedFieldUpdateMsg, e); + throw e; + } + return Pair.of(isCreated, isNameUpdated); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java new file mode 100644 index 0000000000..0ddb62e874 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java @@ -0,0 +1,133 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.calculated; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; + +import java.util.UUID; + +@Slf4j +@Component +@TbCoreComponent +public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor implements CalculatedFieldProcessor { + + @Override + public ListenableFuture processCalculatedFieldMsgFromEdge(TenantId tenantId, Edge edge, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg) { + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(new UUID(calculatedFieldUpdateMsg.getIdMSB(), calculatedFieldUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getEdgeId().set(edge.getId()); + + switch (calculatedFieldUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + processCalculatedField(tenantId, calculatedFieldId, calculatedFieldUpdateMsg, edge); + return Futures.immediateFuture(null); + case ENTITY_DELETED_RPC_MESSAGE: + CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(tenantId, calculatedFieldId); + if (calculatedField != null) { + edgeCtx.getCalculatedFieldService().deleteCalculatedField(tenantId, calculatedFieldId); + } + return Futures.immediateFuture(null); + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(calculatedFieldUpdateMsg.getMsgType()); + } + } catch (DataValidationException e) { + log.warn("[{}] Failed to process CalculatedFieldUpdateMsg from Edge [{}]", tenantId, calculatedFieldUpdateMsg, e); + return Futures.immediateFailedFuture(e); + } finally { + edgeSynchronizationManager.getEdgeId().remove(); + } + } + + @Override + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(edgeEvent.getEntityId()); + switch (edgeEvent.getAction()) { + case ADDED, UPDATED -> { + CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(edgeEvent.getTenantId(), calculatedFieldId); + if (calculatedField != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = EdgeMsgConstructorUtils.constructCalculatedFieldUpdatedMsg(msgType, calculatedField); + return DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addCalculatedFieldUpdateMsg(calculatedFieldUpdateMsg) + .build(); + } + } + case DELETED -> { + CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = EdgeMsgConstructorUtils.constructCalculatedFieldDeleteMsg(calculatedFieldId); + return DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addCalculatedFieldUpdateMsg(calculatedFieldUpdateMsg) + .build(); + } + } + return null; + } + + @Override + public EdgeEventType getEdgeEventType() { + return EdgeEventType.CALCULATED_FIELD; + } + + + private void processCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg, Edge edge) { + Pair resultPair = super.saveOrUpdateCalculatedField(tenantId, calculatedFieldId, calculatedFieldUpdateMsg); + Boolean wasCreated = resultPair.getFirst(); + if (wasCreated) { + pushCalculatedFieldCreatedEventToRuleEngine(tenantId, edge, calculatedFieldId); + } + Boolean nameWasUpdated = resultPair.getSecond(); + if (nameWasUpdated) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.CALCULATED_FIELD, EdgeEventActionType.UPDATED, calculatedFieldId, null); + } + } + + private void pushCalculatedFieldCreatedEventToRuleEngine(TenantId tenantId, Edge edge, CalculatedFieldId calculatedFieldId) { + try { + CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(tenantId, calculatedFieldId); + String calculatedFieldAsString = JacksonUtil.toString(calculatedField); + TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, edge.getCustomerId()); + pushEntityEventToRuleEngine(tenantId, calculatedFieldId, edge.getCustomerId(), TbMsgType.ENTITY_CREATED, calculatedFieldAsString, msgMetaData); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push calculatedField action to rule engine: {}", tenantId, calculatedFieldId, TbMsgType.ENTITY_CREATED.name(), e); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldProcessor.java new file mode 100644 index 0000000000..ba9c8b27e1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldProcessor.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.calculated; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor; + +public interface CalculatedFieldProcessor extends EdgeProcessor { + + ListenableFuture processCalculatedFieldMsgFromEdge(TenantId tenantId, Edge edge, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg); + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java index 5101d6d57e..a645903896 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java @@ -31,8 +31,12 @@ public interface CalculatedFieldService extends EntityDaoService { CalculatedField save(CalculatedField calculatedField); + CalculatedField save(CalculatedField calculatedField, boolean doValidate); + CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId); + CalculatedField findByTenantIdAndName(TenantId tenantId, String name); + List findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId); List findCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java index 6f1ae8150f..9c97935329 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java @@ -41,12 +41,13 @@ public enum EdgeEventType { ADMIN_SETTINGS(true, null), OTA_PACKAGE(true, EntityType.OTA_PACKAGE), QUEUE(true, EntityType.QUEUE), - NOTIFICATION_RULE (true, EntityType.NOTIFICATION_RULE), - NOTIFICATION_TARGET (true, EntityType.NOTIFICATION_TARGET), - NOTIFICATION_TEMPLATE (true, EntityType.NOTIFICATION_TEMPLATE), + NOTIFICATION_RULE(true, EntityType.NOTIFICATION_RULE), + NOTIFICATION_TARGET(true, EntityType.NOTIFICATION_TARGET), + NOTIFICATION_TEMPLATE(true, EntityType.NOTIFICATION_TEMPLATE), TB_RESOURCE(true, EntityType.TB_RESOURCE), OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT), - DOMAIN(true, EntityType.DOMAIN); + DOMAIN(true, EntityType.DOMAIN), + CALCULATED_FIELD(true, EntityType.CALCULATED_FIELD); private final boolean allEdgesRelated; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index f5dd4b12a0..01727b0ebd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -169,6 +169,8 @@ public class EntityIdFactory { return new OAuth2ClientId(uuid); case DOMAIN: return new DomainId(uuid); + case CALCULATED_FIELD: + return new CalculatedFieldId(uuid); } throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); } diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 023ac00634..904a938f1e 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -124,6 +124,14 @@ enum UpdateMsgType { // use 6 as a next number } +message CalculatedFieldUpdateMsg{ + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string entity = 4; +} + + message EntityDataProto { int64 entityIdMSB = 1; int64 entityIdLSB = 2; @@ -423,6 +431,7 @@ message UplinkMsg { repeated AlarmCommentUpdateMsg alarmCommentUpdateMsg = 22; repeated RuleChainUpdateMsg ruleChainUpdateMsg = 23; repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24; + repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25; } message UplinkResponseMsg { @@ -472,4 +481,5 @@ message DownlinkMsg { repeated NotificationTargetUpdateMsg notificationTargetUpdateMsg = 32; repeated NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = 33; repeated OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = 34; + repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 35; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 0c5df18e80..141adf49aa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -57,7 +57,18 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements @Override public CalculatedField save(CalculatedField calculatedField) { - CalculatedField oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); + return doSave(calculatedField, true); + } + + @Override + public CalculatedField save(CalculatedField calculatedField, boolean doValidate) { + return doSave(calculatedField, doValidate); + } + + private CalculatedField doSave(CalculatedField calculatedField, boolean doValidate) { + if (doValidate) { + calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); + } try { TenantId tenantId = calculatedField.getTenantId(); log.trace("Executing save calculated field, [{}]", calculatedField); @@ -65,7 +76,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements CalculatedField savedCalculatedField = calculatedFieldDao.save(tenantId, calculatedField); createOrUpdateCalculatedFieldLink(tenantId, savedCalculatedField); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCalculatedField.getTenantId()).entityId(savedCalculatedField.getId()) - .entity(savedCalculatedField).oldEntity(oldCalculatedField).created(calculatedField.getId() == null).build()); + .entity(savedCalculatedField).oldEntity(calculatedField).created(calculatedField.getId() == null).build()); return savedCalculatedField; } catch (Exception e) { checkConstraintViolation(e, @@ -83,6 +94,14 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return calculatedFieldDao.findById(tenantId, calculatedFieldId.getId()); } + @Override + public CalculatedField findByTenantIdAndName(TenantId tenantId, String name) { + log.trace("Executing findByTenantIdAndName [{}], calculatedFieldName[{}]", tenantId, name); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + + return calculatedFieldDao.findByTenantIdAndName(tenantId, name).orElse(null); + } + @Override public List findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId) { log.trace("Executing findCalculatedFieldIdsByEntityId [{}]", entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java index aadae93893..44dbb26b81 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import java.util.List; +import java.util.Optional; public interface CalculatedFieldDao extends Dao { @@ -35,6 +36,8 @@ public interface CalculatedFieldDao extends Dao { List findAll(); + Optional findByTenantIdAndName(TenantId tenantId, String name); + PageData findAll(PageLink pageLink); PageData findAllByTenantId(TenantId tenantId, PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java index be122816ba..91721c74f3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java @@ -28,6 +28,8 @@ public interface CalculatedFieldRepository extends JpaRepository findCalculatedFieldIdsByTenantIdAndEntityId(UUID tenantId, UUID entityId); List findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java index 4bb52c29db..f18fe10f8c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java @@ -34,6 +34,7 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.List; +import java.util.Optional; import java.util.UUID; @Slf4j @@ -65,6 +66,12 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findByTenantIdAndName(TenantId tenantId, String name) { + CalculatedField calculatedField = DaoUtil.getData(calculatedFieldRepository.findByTenantIdAndName(tenantId.getId(), name)); + return Optional.ofNullable(calculatedField); + } + @Override public PageData findAll(PageLink pageLink) { log.debug("Try to find calculated fields by pageLink [{}]", pageLink); From d45cbcfcbdcf7271300e8fbb5ed8b6a6d3169a3f Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Mon, 2 Jun 2025 14:25:25 +0300 Subject: [PATCH 035/435] CalculatedField functionality support for Edge - fixed issue when entity not assigned to Edge --- .../CalculatedFieldEdgeProcessor.java | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java index 0ddb62e874..f936b0b1d4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java @@ -30,6 +30,8 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; @@ -39,6 +41,7 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; +import java.util.List; import java.util.UUID; @Slf4j @@ -68,8 +71,12 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i return handleUnsupportedMsgType(calculatedFieldUpdateMsg.getMsgType()); } } catch (DataValidationException e) { - log.warn("[{}] Failed to process CalculatedFieldUpdateMsg from Edge [{}]", tenantId, calculatedFieldUpdateMsg, e); - return Futures.immediateFailedFuture(e); + if (e.getMessage().contains("limit reached")) { + log.warn("[{}] Number of allowed calculatedField violated {}", tenantId, calculatedFieldUpdateMsg, e); + return Futures.immediateFuture(null); + } else { + return Futures.immediateFailedFuture(e); + } } finally { edgeSynchronizationManager.getEdgeId().remove(); } @@ -81,7 +88,7 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(edgeEvent.getTenantId(), calculatedFieldId); - if (calculatedField != null) { + if (calculatedField != null && isEntityAssignedToEdge(edgeEvent, calculatedField)) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = EdgeMsgConstructorUtils.constructCalculatedFieldUpdatedMsg(msgType, calculatedField); return DownlinkMsg.newBuilder() @@ -101,6 +108,19 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i return null; } + private boolean isEntityAssignedToEdge(EdgeEvent edgeEvent, CalculatedField calculatedField) { + switch (calculatedField.getEntityId().getEntityType()) { + case ASSET, DEVICE -> { + List relations = + edgeCtx.getRelationService().findByTo(edgeEvent.getTenantId(), calculatedField.getEntityId(), RelationTypeGroup.EDGE); + return !relations.isEmpty(); + } + default -> { + return true; + } + } + } + @Override public EdgeEventType getEdgeEventType() { return EdgeEventType.CALCULATED_FIELD; From a4db17c859797d8c02b8a29d324d6979483869c3 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 2 Jun 2025 16:28:53 +0300 Subject: [PATCH 036/435] AI rule node: add cache to AI settings --- .../src/main/resources/thingsboard.yml | 3 + .../server/common/data/CacheConstants.java | 5 +- .../dao/ai/AiSettingsCacheEvictEvent.java | 29 ++++++++ .../server/dao/ai/AiSettingsCacheKey.java | 47 +++++++++++++ .../dao/ai/AiSettingsCaffeineCache.java | 33 +++++++++ .../server/dao/ai/AiSettingsRedisCache.java | 36 ++++++++++ .../server/dao/ai/AiSettingsServiceImpl.java | 68 ++++++++++++++----- 7 files changed, 202 insertions(+), 19 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheEvictEvent.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCaffeineCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsRedisCache.java diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 5104f2e264..54c1159003 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -647,6 +647,9 @@ cache: trendzSettings: timeToLiveInMinutes: "${CACHE_SPECS_TRENDZ_SETTINGS_TTL:1440}" # Trendz settings cache TTL maxSize: "${CACHE_SPECS_TRENDZ_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled + aiSettings: + timeToLiveInMinutes: "${CACHE_SPECS_AI_SETTINGS_TTL:1440}" # AI settings cache TTL + maxSize: "${CACHE_SPECS_AI_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled # Deliberately placed outside the 'specs' group above notificationRules: diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 5b167c88a2..4b103d769a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -15,7 +15,8 @@ */ package org.thingsboard.server.common.data; -public class CacheConstants { +public final class CacheConstants { + public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials"; public static final String RELATIONS_CACHE = "relations"; public static final String DEVICE_CACHE = "devices"; @@ -36,6 +37,7 @@ public class CacheConstants { public static final String NOTIFICATION_SETTINGS_CACHE = "notificationSettings"; public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications"; public static final String TRENDZ_SETTINGS_CACHE = "trendzSettings"; + public static final String AI_SETTINGS_CACHE = "aiSettings"; public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ATTRIBUTES_CACHE = "attributes"; @@ -54,4 +56,5 @@ public class CacheConstants { public static final String ALARM_TYPES_CACHE = "alarmTypes"; public static final String QR_CODE_SETTINGS_CACHE = "qrCodeSettings"; public static final String MOBILE_SECRET_KEY_CACHE = "mobileSecretKey"; + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheEvictEvent.java new file mode 100644 index 0000000000..d6cad8bf16 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheEvictEvent.java @@ -0,0 +1,29 @@ +/** + * 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 org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.Set; + +record AiSettingsCacheEvictEvent(Set keys) { + + static AiSettingsCacheEvictEvent of(TenantId tenantId, AiSettingsId aiSettingsId) { + return new AiSettingsCacheEvictEvent(Set.of(AiSettingsCacheKey.of(tenantId, aiSettingsId))); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java new file mode 100644 index 0000000000..fcf737d3a5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java @@ -0,0 +1,47 @@ +/** + * 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 org.thingsboard.server.cache.VersionedCacheKey; +import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.UUID; + +import static java.util.Objects.requireNonNull; + +record AiSettingsCacheKey(UUID tenantId, UUID aiSettingsId) implements VersionedCacheKey { + + AiSettingsCacheKey { + requireNonNull(tenantId); + requireNonNull(aiSettingsId); + } + + static AiSettingsCacheKey of(TenantId tenantId, AiSettingsId aiSettingsId) { + return new AiSettingsCacheKey(tenantId.getId(), aiSettingsId.getId()); + } + + @Override + public boolean isVersioned() { + return true; + } + + @Override + public String toString() { + return tenantId + "_" + aiSettingsId; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCaffeineCache.java new file mode 100644 index 0000000000..f006c0a757 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCaffeineCache.java @@ -0,0 +1,33 @@ +/** + * 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 org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Component; +import org.thingsboard.server.cache.VersionedCaffeineTbCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.ai.AiSettings; + +@Component("AiSettingsCache") +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +class AiSettingsCaffeineCache extends VersionedCaffeineTbCache { + + AiSettingsCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.AI_SETTINGS_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsRedisCache.java new file mode 100644 index 0000000000..0c1da26769 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsRedisCache.java @@ -0,0 +1,36 @@ +/** + * 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 org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Component; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbJsonRedisSerializer; +import org.thingsboard.server.cache.VersionedRedisTbCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.ai.AiSettings; + +@Component("AiSettingsCache") +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +class AiSettingsRedisCache extends VersionedRedisTbCache { + + AiSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.AI_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(AiSettings.class)); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java index 1bfa316c73..517d23c496 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java @@ -15,10 +15,11 @@ */ package org.thingsboard.server.dao.ai; +import com.google.common.collect.Sets; import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ai.AiSettings; import org.thingsboard.server.common.data.id.AiSettingsId; @@ -27,26 +28,33 @@ 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.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.service.DataValidator; import java.util.List; import java.util.Optional; +import java.util.Set; -import static org.thingsboard.server.dao.entity.AbstractEntityService.checkConstraintViolation; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @Service @RequiredArgsConstructor -class AiSettingsServiceImpl implements AiSettingsService { +class AiSettingsServiceImpl extends CachedVersionedEntityService implements AiSettingsService { - private final ApplicationEventPublisher eventPublisher; private final DataValidator aiSettingsValidator; private final AiSettingsDao aiSettingsDao; @Override + @TransactionalEventListener + public void handleEvictEvent(AiSettingsCacheEvictEvent event) { + cache.evict(event.keys()); + } + + @Override + @Transactional public AiSettings save(AiSettings aiSettings) { AiSettings oldSettings = aiSettingsValidator.validate(aiSettings, AiSettings::getTenantId); @@ -58,15 +66,22 @@ class AiSettingsServiceImpl implements AiSettingsService { throw e; } + boolean created = oldSettings == null; + boolean updated = oldSettings != null; + eventPublisher.publishEvent(SaveEntityEvent.builder() .tenantId(savedSettings.getTenantId()) .entity(savedSettings) .oldEntity(oldSettings) .entityId(savedSettings.getId()) - .created(oldSettings == null) + .created(created) .broadcastEvent(true) .build()); + if (updated) { + publishEvictEvent(AiSettingsCacheEvictEvent.of(savedSettings.getTenantId(), savedSettings.getId())); + } + return savedSettings; } @@ -83,25 +98,20 @@ class AiSettingsServiceImpl implements AiSettingsService { @Override public Optional findAiSettingsByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { - return aiSettingsDao.findByTenantIdAndId(tenantId, aiSettingsId); + var cacheKey = AiSettingsCacheKey.of(tenantId, aiSettingsId); + return Optional.ofNullable(cache.get(cacheKey, () -> aiSettingsDao.findByTenantIdAndId(tenantId, aiSettingsId).orElse(null))); } @Override + @Transactional public boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { - Optional aiSettingsOpt = aiSettingsDao.findByTenantIdAndId(tenantId, aiSettingsId); - if (aiSettingsOpt.isEmpty()) { - return false; - } - boolean deleted = aiSettingsDao.deleteByTenantIdAndId(tenantId, aiSettingsId); - if (deleted) { - publishDeleteEvent(aiSettingsOpt.get()); - } - return deleted; + return deleteByTenantIdAndIdInternal(tenantId, aiSettingsId); } @Override public Optional> findEntity(TenantId tenantId, EntityId entityId) { - return Optional.ofNullable(aiSettingsDao.findById(tenantId, entityId.getId())); + return findAiSettingsByTenantIdAndId(tenantId, (AiSettingsId) entityId) + .map(aiSettings -> aiSettings); // necessary to cast to HasId } @Override @@ -110,8 +120,22 @@ class AiSettingsServiceImpl implements AiSettingsService { } @Override + @Transactional public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { - deleteByTenantIdAndId(tenantId, new AiSettingsId(id.getId())); + deleteByTenantIdAndIdInternal(tenantId, new AiSettingsId(id.getId())); + } + + private boolean deleteByTenantIdAndIdInternal(TenantId tenantId, AiSettingsId aiSettingsId) { + Optional aiSettingsOpt = aiSettingsDao.findByTenantIdAndId(tenantId, aiSettingsId); + if (aiSettingsOpt.isEmpty()) { + return false; + } + boolean deleted = aiSettingsDao.deleteByTenantIdAndId(tenantId, aiSettingsId); + if (deleted) { + publishDeleteEvent(aiSettingsOpt.get()); + publishEvictEvent(AiSettingsCacheEvictEvent.of(tenantId, aiSettingsId)); + } + return deleted; } @Override @@ -121,8 +145,16 @@ class AiSettingsServiceImpl implements AiSettingsService { if (deletedSettings.isEmpty()) { return; } + aiSettingsDao.deleteByTenantId(tenantId); - deletedSettings.forEach(this::publishDeleteEvent); + + Set cacheKeys = Sets.newHashSetWithExpectedSize(deletedSettings.size()); + deletedSettings.forEach(settings -> { + publishDeleteEvent(settings); + cacheKeys.add(AiSettingsCacheKey.of(settings.getTenantId(), settings.getId())); + }); + + publishEvictEvent(new AiSettingsCacheEvictEvent(cacheKeys)); } private void publishDeleteEvent(AiSettings settings) { From 93948bf64bd57306d7715c8337e854e4205d0417 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Mon, 2 Jun 2025 18:34:32 +0300 Subject: [PATCH 037/435] CalculatedField functionality support for Edge - added test --- .../server/edge/AbstractEdgeTest.java | 2 +- .../server/edge/CalculatedFieldEdgeTest.java | 228 ++++++++++++++++++ .../server/edge/imitator/EdgeImitator.java | 6 + .../dao/cf/BaseCalculatedFieldService.java | 17 +- 4 files changed, 246 insertions(+), 7 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index c02e72a35f..24fa02f11a 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -565,7 +565,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { protected Device saveDeviceOnCloudAndVerifyDeliveryToEdge() throws Exception { // create device and assign to edge Device savedDevice = saveDevice(StringUtils.randomAlphanumeric(15), thermostatDeviceProfile.getName()); - edgeImitator.expectMessageAmount(2); // device and device profile messages + edgeImitator.expectMessageAmount(3); // device and device profile messages and device credentials doPost("/api/edge/" + edge.getUuidId() + "/device/" + savedDevice.getUuidId(), Device.class); Assert.assertTrue(edgeImitator.waitForMessages()); diff --git a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java new file mode 100644 index 0000000000..0f785c4063 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java @@ -0,0 +1,228 @@ +/** + * 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.edge; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.debug.DebugSettings; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; + +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class CalculatedFieldEdgeTest extends AbstractEdgeTest { + private static final String DEFAULT_CF_NAME = "Edge Test CalculatedField"; + private static final String UPDATED_CF_NAME = "Updated Edge Test CalculatedField"; + + @Test + public void testCalculatedField_create_update_delete() throws Exception { + Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); + + // create calculatedField + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); + + edgeImitator.expectMessageAmount(1); + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); + CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); + Assert.assertEquals(savedCalculatedField.getUuidId().getMostSignificantBits(), calculatedFieldUpdateMsg.getIdMSB()); + Assert.assertEquals(savedCalculatedField.getUuidId().getLeastSignificantBits(), calculatedFieldUpdateMsg.getIdLSB()); + CalculatedField calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); + Assert.assertNotNull(calculatedFieldFromMsg); + + Assert.assertEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName()); + Assert.assertEquals(savedDevice.getId(), calculatedFieldFromMsg.getEntityId()); + Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration()); + + // update calculatedField + edgeImitator.expectMessageAmount(1); + savedCalculatedField.setName(UPDATED_CF_NAME); + savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); + calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; + calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); + Assert.assertNotNull(calculatedFieldFromMsg); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); + Assert.assertEquals(UPDATED_CF_NAME, calculatedFieldFromMsg.getName()); + + // delete calculatedField + edgeImitator.expectMessageAmount(1); + doDelete("/api/calculatedField/" + savedCalculatedField.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); + calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); + Assert.assertEquals(savedCalculatedField.getUuidId().getMostSignificantBits(), calculatedFieldUpdateMsg.getIdMSB()); + Assert.assertEquals(savedCalculatedField.getUuidId().getLeastSignificantBits(), calculatedFieldUpdateMsg.getIdLSB()); + } + + @Test + public void testSendCalculatedFieldToCloud() throws Exception { + Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); + + // create calculatedField + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); + UUID uuid = Uuids.timeBased(); + UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + + checkCalculatedFieldOnCloud(uplinkMsg, uuid, calculatedField.getName()); + } + + @Test + public void testUpdateCalculatedFieldNameOnCloud() throws Exception { + Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); + + // create calculatedField + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); + UUID uuid = Uuids.timeBased(); + UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + + checkCalculatedFieldOnCloud(uplinkMsg, uuid, calculatedField.getName()); + + calculatedField.setName(UPDATED_CF_NAME); + UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE); + + checkCalculatedFieldOnCloud(updatedUplinkMsg, uuid, calculatedField.getName()); + } + + @Test + public void testCalculatedFieldToCloudWithNameThatAlreadyExistsOnCloud() throws Exception { + Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); + + // create calculatedField + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); + + edgeImitator.expectMessageAmount(1); + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + + edgeImitator.sendUplinkMsg(uplinkMsg); + + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + Optional calculatedFieldUpdateMsgOpt = edgeImitator.findMessageByType(CalculatedFieldUpdateMsg.class); + Assert.assertTrue(calculatedFieldUpdateMsgOpt.isPresent()); + CalculatedFieldUpdateMsg latestCalculatedFieldUpdateMsg = calculatedFieldUpdateMsgOpt.get(); + CalculatedField calculatedFieldFromMsg = JacksonUtil.fromString(latestCalculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); + Assert.assertNotNull(calculatedFieldFromMsg); + Assert.assertNotEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName()); + + Assert.assertNotEquals(savedCalculatedField.getUuidId(), uuid); + + CalculatedField calculatedFieldFromCloud = doGet("/api/calculatedField/" + uuid, CalculatedField.class); + Assert.assertNotNull(calculatedFieldFromCloud); + Assert.assertNotEquals(DEFAULT_CF_NAME, calculatedFieldFromCloud.getName()); + } + + private CalculatedField createSimpleCalculatedField(EntityId entityId, SimpleCalculatedFieldConfiguration config) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(entityId); + calculatedField.setTenantId(tenantId); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName(DEFAULT_CF_NAME); + calculatedField.setDebugSettings(DebugSettings.all()); + + Argument argument = new Argument(); + ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); + argument.setRefEntityKey(refEntityKey); + argument.setDefaultValue("12"); // not used because real telemetry value in db is present + config.setArguments(Map.of("T", argument)); + + config.setExpression("(T * 9/5) + 32"); + + Output output = new Output(); + output.setName("fahrenheitTemp"); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(2); + config.setOutput(output); + + calculatedField.setConfiguration(config); + + return calculatedField; + } + + private UplinkMsg getUplinkMsg(UUID uuid, CalculatedField calculatedField, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException { + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + CalculatedFieldUpdateMsg.Builder calculatedFieldUpdateMsgBuilder = CalculatedFieldUpdateMsg.newBuilder(); + calculatedFieldUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + calculatedFieldUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + calculatedFieldUpdateMsgBuilder.setEntity(JacksonUtil.toString(calculatedField)); + calculatedFieldUpdateMsgBuilder.setMsgType(updateMsgType); + testAutoGeneratedCodeByProtobuf(calculatedFieldUpdateMsgBuilder); + uplinkMsgBuilder.addCalculatedFieldUpdateMsg(calculatedFieldUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + return uplinkMsgBuilder.build(); + } + + private void checkCalculatedFieldOnCloud(UplinkMsg uplinkMsg, UUID uuid, String resourceTitle) throws Exception { + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsg); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); + + CalculatedField calculatedField = doGet("/api/calculatedField/" + uuid, CalculatedField.class); + Assert.assertNotNull(calculatedField); + Assert.assertEquals(resourceTitle, calculatedField.getName()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java index 8b259cf8fc..16d10d9b6e 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -33,6 +33,7 @@ import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; @@ -352,6 +353,11 @@ public class EdgeImitator { result.add(saveDownlinkMsg(notificationTargetUpdateMsg)); } } + if (downlinkMsg.getCalculatedFieldUpdateMsgCount() > 0) { + for (CalculatedFieldUpdateMsg calculatedFieldUpdateMsg : downlinkMsg.getCalculatedFieldUpdateMsgList()) { + result.add(saveDownlinkMsg(calculatedFieldUpdateMsg)); + } + } if (downlinkMsg.hasEdgeConfiguration()) { result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration())); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 141adf49aa..5d4d3c54da 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -57,18 +57,23 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements @Override public CalculatedField save(CalculatedField calculatedField) { - return doSave(calculatedField, true); + CalculatedField oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); + + return doSave(calculatedField, oldCalculatedField); } @Override public CalculatedField save(CalculatedField calculatedField, boolean doValidate) { - return doSave(calculatedField, doValidate); - } + CalculatedField oldCalculatedField = null; - private CalculatedField doSave(CalculatedField calculatedField, boolean doValidate) { if (doValidate) { - calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); + oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); } + + return doSave(calculatedField, oldCalculatedField); + } + + private CalculatedField doSave(CalculatedField calculatedField, CalculatedField oldCalculatedField) { try { TenantId tenantId = calculatedField.getTenantId(); log.trace("Executing save calculated field, [{}]", calculatedField); @@ -76,7 +81,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements CalculatedField savedCalculatedField = calculatedFieldDao.save(tenantId, calculatedField); createOrUpdateCalculatedFieldLink(tenantId, savedCalculatedField); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCalculatedField.getTenantId()).entityId(savedCalculatedField.getId()) - .entity(savedCalculatedField).oldEntity(calculatedField).created(calculatedField.getId() == null).build()); + .entity(savedCalculatedField).oldEntity(oldCalculatedField).created(calculatedField.getId() == null).build()); return savedCalculatedField; } catch (Exception e) { checkConstraintViolation(e, From 2761866e5838e3cd28ed8c5de5c32453086e3779 Mon Sep 17 00:00:00 2001 From: yevhenii Date: Thu, 5 Jun 2025 18:06:51 +0300 Subject: [PATCH 038/435] CalculatedField functionality support for Edge - Modified downlink push logic --- .../edge/EdgeEventSourcingListener.java | 3 ++ .../service/edge/rpc/EdgeSyncCursor.java | 2 - .../CalculatedFieldsEdgeEventFetcher.java | 48 ------------------ .../edge/rpc/processor/BaseEdgeProcessor.java | 11 ++++ .../processor/asset/AssetEdgeProcessor.java | 3 ++ .../BaseCalculatedFieldProcessor.java | 31 ++++++++++++ .../CalculatedFieldEdgeProcessor.java | 50 ++++++++++++------- .../processor/device/DeviceEdgeProcessor.java | 3 ++ .../common/data/edge/EdgeEventType.java | 2 +- 9 files changed, 85 insertions(+), 68 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CalculatedFieldsEdgeEventFetcher.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index 7bafd53644..70c63a96cb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -262,6 +263,8 @@ public class EdgeEventSourcingListener { private String getBodyMsgForEntityEvent(Object entity) { if (entity instanceof AlarmComment) { return JacksonUtil.toString(entity); + } else if (entity instanceof CalculatedField calculatedField) { + return JacksonUtil.toString(calculatedField.getEntityId()); } return null; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index 389f0202fa..adab9b812f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -23,7 +23,6 @@ import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.edge.rpc.fetch.AdminSettingsEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.AssetProfilesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.AssetsEdgeEventFetcher; -import org.thingsboard.server.service.edge.rpc.fetch.CalculatedFieldsEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.CustomerEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.CustomerUsersEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.DashboardsEdgeEventFetcher; @@ -81,7 +80,6 @@ public class EdgeSyncCursor { fetchers.add(new DevicesEdgeEventFetcher(ctx.getDeviceService())); fetchers.add(new AssetsEdgeEventFetcher(ctx.getAssetService())); fetchers.add(new EntityViewsEdgeEventFetcher(ctx.getEntityViewService())); - fetchers.add(new CalculatedFieldsEdgeEventFetcher(ctx.getCalculatedFieldService())); if (fullSync) { fetchers.add(new NotificationTemplateEdgeEventFetcher(ctx.getNotificationTemplateService())); fetchers.add(new NotificationTargetEdgeEventFetcher(ctx.getNotificationTargetService())); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CalculatedFieldsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CalculatedFieldsEdgeEventFetcher.java deleted file mode 100644 index 4f3e91354e..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CalculatedFieldsEdgeEventFetcher.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge.rpc.fetch; - -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEvent; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.edge.EdgeEventType; -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.cf.CalculatedFieldService; - -@AllArgsConstructor -@Slf4j -public class CalculatedFieldsEdgeEventFetcher extends BasePageableEdgeEventFetcher { - - private final CalculatedFieldService calculatedFieldService; - - @Override - PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { - return calculatedFieldService.findCalculatedFieldsByTenantId(tenantId, pageLink); - } - - @Override - EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, CalculatedField calculatedField) { - return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.CALCULATED_FIELD, - EdgeEventActionType.ADDED, calculatedField.getId(), null); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 6fcb02e4bc..077ff21d26 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -25,6 +25,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -53,11 +54,13 @@ import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; import org.thingsboard.server.dao.entity.EntityDaoRegistry; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.service.edge.EdgeContextComponent; +import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.state.DefaultDeviceStateService; @@ -381,4 +384,12 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor { }); } + protected List getCalculatedFieldUpdateMsgs(TenantId tenantId, EntityId entityId) { + List calculatedFields = edgeCtx.getCalculatedFieldService().findCalculatedFieldsByEntityId(tenantId, entityId); + + return calculatedFields.stream() + .map(calculatedField -> EdgeMsgConstructorUtils.constructCalculatedFieldUpdatedMsg(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, calculatedField)) + .toList(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index 47f4c11362..538a90f7d7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -119,6 +119,9 @@ public class AssetEdgeProcessor extends BaseAssetProcessor implements AssetProce DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addAssetUpdateMsg(assetUpdateMsg); + + getCalculatedFieldUpdateMsgs(edgeEvent.getTenantId(), assetId).forEach(builder::addCalculatedFieldUpdateMsg); + if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { AssetProfile assetProfile = edgeCtx.getAssetProfileService().findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId()); builder.addAssetProfileUpdateMsg(EdgeMsgConstructorUtils.constructAssetProfileUpdatedMsg(msgType, assetProfile)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java index ddb1b23d53..63cc1e024d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java @@ -16,18 +16,30 @@ package org.thingsboard.server.service.edge.rpc.processor.calculated; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.util.Pair; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; +import java.util.ArrayList; +import java.util.List; + +import static org.thingsboard.server.dao.edge.BaseRelatedEdgesService.RELATED_EDGES_CACHE_ITEMS; + @Slf4j public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { @@ -76,4 +88,23 @@ public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { return Pair.of(isCreated, isNameUpdated); } + protected ListenableFuture pushEventToAllRelatedEdges(TenantId tenantId, EntityId entityId, EdgeEventType type, EdgeEventActionType actionType, EdgeId sourceEdgeId) { + List> futures = new ArrayList<>(); + PageDataIterableByTenantIdEntityId edgeIds = + new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, entityId, RELATED_EDGES_CACHE_ITEMS); + for (EdgeId relatedEdgeId : edgeIds) { + if (!relatedEdgeId.equals(sourceEdgeId)) { + futures.add(saveEdgeEvent(tenantId, relatedEdgeId, type, actionType, entityId, null)); + } + } + return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); + } + + protected ListenableFuture pushEventToAllEdges(TenantId tenantId, EdgeEventType type, EdgeEventActionType actionType, EntityId entityId, EdgeId sourceEdgeId) { + return switch (actionType) { + case ADDED, UPDATED, DELETED -> processActionForAllEdges(tenantId, type, actionType, entityId, null, sourceEdgeId); + default -> Futures.immediateFuture(null); + }; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java index f936b0b1d4..cf28003707 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.edge.rpc.processor.calculated; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; @@ -22,26 +23,28 @@ import org.springframework.data.util.Pair; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; -import java.util.List; import java.util.UUID; @Slf4j @@ -88,7 +91,7 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(edgeEvent.getTenantId(), calculatedFieldId); - if (calculatedField != null && isEntityAssignedToEdge(edgeEvent, calculatedField)) { + if (calculatedField != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = EdgeMsgConstructorUtils.constructCalculatedFieldUpdatedMsg(msgType, calculatedField); return DownlinkMsg.newBuilder() @@ -108,24 +111,37 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i return null; } - private boolean isEntityAssignedToEdge(EdgeEvent edgeEvent, CalculatedField calculatedField) { - switch (calculatedField.getEntityId().getEntityType()) { - case ASSET, DEVICE -> { - List relations = - edgeCtx.getRelationService().findByTo(edgeEvent.getTenantId(), calculatedField.getEntityId(), RelationTypeGroup.EDGE); - return !relations.isEmpty(); - } - default -> { - return true; - } - } - } - @Override public EdgeEventType getEdgeEventType() { return EdgeEventType.CALCULATED_FIELD; } + @Override + public ListenableFuture processEntityNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); + EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); + EntityId entityId = EntityIdFactory.getByEdgeEventTypeAndUuid(type, new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); + EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB()); + + switch (actionType) { + case UPDATED: + case ADDED: + EntityId bodyEntityId = JacksonUtil.fromString(edgeNotificationMsg.getBody(), EntityId.class); + if (bodyEntityId != null && + (EntityType.DEVICE.equals(bodyEntityId.getEntityType()) || EntityType.ASSET.equals(bodyEntityId.getEntityType()))) { + JsonNode body = JacksonUtil.toJsonNode(edgeNotificationMsg.getBody()); + EdgeId edgeId = safeGetEdgeId(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB()); + + return edgeId != null ? + saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body) : + pushEventToAllRelatedEdges(tenantId, entityId, type, actionType, originatorEdgeId); + } else { + return pushEventToAllEdges(tenantId, type, actionType, entityId, originatorEdgeId); + } + default: + return super.processEntityNotification(tenantId, edgeNotificationMsg); + } + } private void processCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg, Edge edge) { Pair resultPair = super.saveOrUpdateCalculatedField(tenantId, calculatedFieldId, calculatedFieldUpdateMsg); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index ab01f83cd8..be2a04626c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -243,6 +243,9 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = EdgeMsgConstructorUtils.constructDeviceCredentialsUpdatedMsg(deviceCredentials); builder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg).build(); } + + getCalculatedFieldUpdateMsgs(edgeEvent.getTenantId(), deviceId).forEach(builder::addCalculatedFieldUpdateMsg); + if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { DeviceProfile deviceProfile = edgeCtx.getDeviceProfileService().findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); builder.addDeviceProfileUpdateMsg(EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java index 9c97935329..0d5c3f34ad 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java @@ -47,7 +47,7 @@ public enum EdgeEventType { TB_RESOURCE(true, EntityType.TB_RESOURCE), OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT), DOMAIN(true, EntityType.DOMAIN), - CALCULATED_FIELD(true, EntityType.CALCULATED_FIELD); + CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD); private final boolean allEdgesRelated; From 411943d993b54d45e42e3ec4d3b57b7f837a41dc Mon Sep 17 00:00:00 2001 From: yevhenii Date: Thu, 5 Jun 2025 19:00:10 +0300 Subject: [PATCH 039/435] CalculatedField functionality support for Edge - fixed test --- .../server/edge/CalculatedFieldEdgeTest.java | 18 ++++++++++++------ .../server/dao/edge/EdgeServiceImpl.java | 1 + 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java index 0f785c4063..da75225d11 100644 --- a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java @@ -38,6 +38,7 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -57,14 +58,16 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(SYNC_MESSAGE_COUNT + 4); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + doPost("/api/edge/sync/" + edge.getId()); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + List downlinkMsgs = edgeImitator.getDownlinkMsgs(); + AbstractMessage latestMessage = downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg instanceof CalculatedFieldUpdateMsg).findFirst().get(); Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; - Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); Assert.assertEquals(savedCalculatedField.getUuidId().getMostSignificantBits(), calculatedFieldUpdateMsg.getIdMSB()); Assert.assertEquals(savedCalculatedField.getUuidId().getLeastSignificantBits(), calculatedFieldUpdateMsg.getIdLSB()); CalculatedField calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); @@ -75,11 +78,13 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration()); // update calculatedField - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(SYNC_MESSAGE_COUNT + 4); savedCalculatedField.setName(UPDATED_CF_NAME); savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); + doPost("/api/edge/sync/" + edge.getId()); Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); + downlinkMsgs = edgeImitator.getDownlinkMsgs(); + latestMessage = downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg instanceof CalculatedFieldUpdateMsg).findFirst().get(); Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); @@ -139,8 +144,9 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(SYNC_MESSAGE_COUNT + 4); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + doPost("/api/edge/sync/" + edge.getId()); Assert.assertTrue(edgeImitator.waitForMessages()); UUID uuid = Uuids.timeBased(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 0655d05572..ebcfd7678a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -524,6 +524,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService Date: Mon, 9 Jun 2025 13:25:22 +0300 Subject: [PATCH 040/435] CalculatedField functionality support for Edge - changed find by TenantId to EntityId --- .../calculated/BaseCalculatedFieldProcessor.java | 2 +- .../thingsboard/server/dao/cf/CalculatedFieldService.java | 2 +- .../server/dao/cf/BaseCalculatedFieldService.java | 8 ++++---- .../org/thingsboard/server/dao/cf/CalculatedFieldDao.java | 2 +- .../server/dao/sql/cf/CalculatedFieldRepository.java | 2 +- .../server/dao/sql/cf/JpaCalculatedFieldDao.java | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java index 63cc1e024d..da3051045f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java @@ -65,7 +65,7 @@ public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { } String calculatedFieldName = calculatedField.getName(); - CalculatedField calculatedFieldByName = edgeCtx.getCalculatedFieldService().findByTenantIdAndName(tenantId, calculatedFieldName); + CalculatedField calculatedFieldByName = edgeCtx.getCalculatedFieldService().findByEntityIdAndName(calculatedField.getEntityId(), calculatedFieldName); if (calculatedFieldByName != null && !calculatedFieldByName.getId().equals(calculatedFieldId)) { calculatedFieldName = calculatedFieldName + "_" + StringUtils.randomAlphabetic(15); log.warn("[{}] calculatedField with name {} already exists. Renaming calculatedField name to {}", diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java index a645903896..85cd8d24fd 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java @@ -35,7 +35,7 @@ public interface CalculatedFieldService extends EntityDaoService { CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId); - CalculatedField findByTenantIdAndName(TenantId tenantId, String name); + CalculatedField findByEntityIdAndName(EntityId entityId, String name); List findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 5d4d3c54da..793f839fb9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -100,11 +100,11 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements } @Override - public CalculatedField findByTenantIdAndName(TenantId tenantId, String name) { - log.trace("Executing findByTenantIdAndName [{}], calculatedFieldName[{}]", tenantId, name); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + public CalculatedField findByEntityIdAndName(EntityId entityId, String name) { + log.trace("Executing findByEntityIdAndName [{}], calculatedFieldName[{}]", entityId, name); + validateId(entityId.getId(), id -> INCORRECT_ENTITY_ID + id); - return calculatedFieldDao.findByTenantIdAndName(tenantId, name).orElse(null); + return calculatedFieldDao.findByEntityIdAndName(entityId, name).orElse(null); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java index 44dbb26b81..bd244b9d24 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java @@ -36,7 +36,7 @@ public interface CalculatedFieldDao extends Dao { List findAll(); - Optional findByTenantIdAndName(TenantId tenantId, String name); + Optional findByEntityIdAndName(EntityId entityId, String name); PageData findAll(PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java index 91721c74f3..8ccdb88db0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java @@ -28,7 +28,7 @@ public interface CalculatedFieldRepository extends JpaRepository findCalculatedFieldIdsByTenantIdAndEntityId(UUID tenantId, UUID entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java index f18fe10f8c..23a146bc47 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java @@ -67,8 +67,8 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findByTenantIdAndName(TenantId tenantId, String name) { - CalculatedField calculatedField = DaoUtil.getData(calculatedFieldRepository.findByTenantIdAndName(tenantId.getId(), name)); + public Optional findByEntityIdAndName(EntityId entityId, String name) { + CalculatedField calculatedField = DaoUtil.getData(calculatedFieldRepository.findByEntityIdAndName(entityId.getId(), name)); return Optional.ofNullable(calculatedField); } From 00d32c23741b95608129638cabb6b5c73150d3ff Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 9 Jun 2025 17:11:52 +0300 Subject: [PATCH 041/435] AI rule node: fix wrapping in JSON object if response is not a valid JSON object --- .../main/java/org/thingsboard/rule/engine/ai/TbAiNode.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 315a02a3b9..564de8d66e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -115,7 +115,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { addCallback(sendChatRequest(ctx, chatRequest), new FutureCallback<>() { @Override public void onSuccess(String response) { - if (!isValidJson(response)) { + if (!isValidJsonObject(response)) { response = wrapInJsonObject(response); } tellSuccess(ctx, ackedMsg.transform() @@ -134,9 +134,10 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { return ctx.getExternalCallExecutor().submit(() -> chatModel.chat(chatRequest).aiMessage().text()); } - private static boolean isValidJson(String jsonString) { + private static boolean isValidJsonObject(String jsonString) { try { - return JacksonUtil.toJsonNode(jsonString) != null; + JsonNode result = JacksonUtil.toJsonNode(jsonString); + return result != null && result.isObject(); } catch (IllegalArgumentException e) { return false; } From 109c87486118171d11e3c07770b928d8b446d011 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 9 Jun 2025 17:14:39 +0300 Subject: [PATCH 042/435] AI rule node: fix cache evict when creating new entity --- .../thingsboard/server/dao/ai/AiSettingsCacheKey.java | 2 ++ .../thingsboard/server/dao/ai/AiSettingsServiceImpl.java | 9 ++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java index fcf737d3a5..84ad396766 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java @@ -15,6 +15,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.AiSettingsId; import org.thingsboard.server.common.data.id.TenantId; @@ -39,6 +40,7 @@ record AiSettingsCacheKey(UUID tenantId, UUID aiSettingsId) implements Versioned return true; } + @NonNull @Override public String toString() { return tenantId + "_" + aiSettingsId; diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java index 517d23c496..15e1074b96 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java @@ -66,21 +66,16 @@ class AiSettingsServiceImpl extends CachedVersionedEntityService Date: Mon, 9 Jun 2025 17:33:04 +0300 Subject: [PATCH 043/435] AI rule node: improve text search to be based on provider and model as well --- .../thingsboard/server/controller/ControllerConstants.java | 2 +- .../thingsboard/server/dao/sql/ai/AiSettingsRepository.java | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index 097cb4e577..b27c08b52c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/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_SETTINGS_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the AI settings name"; + protected static final String AI_SETTINGS_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the AI settings name, provider and model."; 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."; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java index 691709f8b8..a73569eac2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java @@ -33,7 +33,10 @@ public interface AiSettingsRepository extends JpaRepository findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); Optional findByTenantIdAndId(UUID tenantId, UUID id); From a9633cf1cecfb83948bf1af439ec6459eee89f2f Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 9 Jun 2025 17:38:41 +0300 Subject: [PATCH 044/435] AI rule node: improve cache key readability --- .../java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java index 84ad396766..d732c08fc8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java @@ -43,7 +43,7 @@ record AiSettingsCacheKey(UUID tenantId, UUID aiSettingsId) implements Versioned @NonNull @Override public String toString() { - return tenantId + "_" + aiSettingsId; + return /* cache name */ "_" + tenantId + "_" + aiSettingsId; } } From 19c234fcdf58175cd1d9401a91c194ac4c01081c Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 9 Jun 2025 17:51:24 +0300 Subject: [PATCH 045/435] AI rule node: configure chat model on each message --- .../org/thingsboard/rule/engine/ai/TbAiNode.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 564de8d66e..6b0bb3803b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -35,6 +35,7 @@ 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.id.AiSettingsId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.dao.exception.DataValidationException; @@ -59,7 +60,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { private SystemMessage systemMessage; private PromptTemplate userPromptTemplate; private ResponseFormat responseFormat; - private ChatModel chatModel; + private AiSettingsId aiSettingsId; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { @@ -86,7 +87,8 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { Rule engine message type: {{msgType}}""" .formatted(config.getUserPrompt()) ); - chatModel = ctx.getAiService().configureChatModel(ctx.getTenantId(), config.getAiSettingsId()); + + aiSettingsId = config.getAiSettingsId(); } private static JsonSchema getJsonSchema(ResponseFormatType responseFormatType, JsonNode jsonSchema) { @@ -112,7 +114,9 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { .responseFormat(responseFormat) .build(); - addCallback(sendChatRequest(ctx, chatRequest), new FutureCallback<>() { + ChatModel chatModel = ctx.getAiService().configureChatModel(ctx.getTenantId(), aiSettingsId); + + addCallback(sendChatRequest(ctx, chatModel, chatRequest), new FutureCallback<>() { @Override public void onSuccess(String response) { if (!isValidJsonObject(response)) { @@ -130,7 +134,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { }, directExecutor()); } - private ListenableFuture sendChatRequest(TbContext ctx, ChatRequest chatRequest) { + private ListenableFuture sendChatRequest(TbContext ctx, ChatModel chatModel, ChatRequest chatRequest) { return ctx.getExternalCallExecutor().submit(() -> chatModel.chat(chatRequest).aiMessage().text()); } @@ -153,7 +157,6 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { systemMessage = null; userPromptTemplate = null; responseFormat = null; - chatModel = null; } } From d5c6ed1f618e20c094296de98501a152aaff31f4 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 10 Jun 2025 12:18:21 +0300 Subject: [PATCH 046/435] AI rule node: JSON ignore validation method in rule node config --- .../org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index c3234f61bb..f9341ae982 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -15,6 +15,7 @@ */ package org.thingsboard.rule.engine.ai; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; import dev.langchain4j.model.chat.request.ResponseFormatType; import jakarta.validation.constraints.AssertTrue; @@ -45,6 +46,7 @@ public class TbAiNodeConfiguration implements NodeConfiguration Date: Wed, 11 Jun 2025 09:54:33 +0300 Subject: [PATCH 047/435] Ota package vc support --- .../main/data/upgrade/basic/schema_update.sql | 49 ++--------- .../DefaultEntitiesExportImportService.java | 2 +- .../exporting/impl/DeviceExportService.java | 2 + .../impl/DeviceProfileExportService.java | 2 + .../impl/OtaPackageExportService.java | 44 ++++++++++ .../impl/BaseEntityImportService.java | 15 ++-- .../importing/impl/DeviceImportService.java | 4 +- .../impl/DeviceProfileImportService.java | 6 +- .../impl/OtaPackageImportService.java | 83 +++++++++++++++++++ .../sync/ie/ExportImportServiceSqlTest.java | 29 +++++-- .../service/sync/vc/VersionControlTest.java | 75 +++++++++++++++-- .../server/dao/ota/OtaPackageService.java | 3 + .../server/common/data/OtaPackage.java | 3 + .../server/common/data/OtaPackageInfo.java | 22 ++++- .../server/common/data/TbResource.java | 3 + .../server/common/data/sync/JsonTbEntity.java | 4 +- .../dao/model/sql/OtaPackageEntity.java | 7 ++ .../dao/model/sql/OtaPackageInfoEntity.java | 9 +- .../server/dao/ota/BaseOtaPackageService.java | 37 ++++----- .../server/dao/ota/OtaPackageDao.java | 7 +- .../server/dao/sql/ota/JpaOtaPackageDao.java | 46 ++++++++-- .../dao/sql/ota/OtaPackageInfoRepository.java | 7 +- .../dao/sql/ota/OtaPackageRepository.java | 12 ++- .../resources/sql/schema-entities-idx.sql | 2 + .../main/resources/sql/schema-entities.sql | 4 +- 25 files changed, 370 insertions(+), 107 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index e4bbe4c69e..2f652ce289 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -14,48 +14,11 @@ -- limitations under the License. -- --- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS START +-- UPDATE OTA PACKAGE EXTERNAL ID START -UPDATE tenant_profile -SET profile_data = jsonb_set( - profile_data, - '{configuration}', - ( - (profile_data -> 'configuration') - 'cassandraQueryTenantRateLimitsConfiguration' - || - COALESCE( - CASE - WHEN profile_data -> 'configuration' -> - 'cassandraQueryTenantRateLimitsConfiguration' IS NOT NULL THEN - jsonb_build_object( - 'cassandraReadQueryTenantCoreRateLimits', - profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration', - 'cassandraWriteQueryTenantCoreRateLimits', - profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration', - 'cassandraReadQueryTenantRuleEngineRateLimits', - profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration', - 'cassandraWriteQueryTenantRuleEngineRateLimits', - profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration' - ) - END, - '{}'::jsonb - ) - ) - ) -WHERE profile_data -> 'configuration' ? 'cassandraQueryTenantRateLimitsConfiguration'; +ALTER TABLE ota_package + ADD COLUMN IF NOT EXISTS external_id uuid; +ALTER TABLE ota_package + ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id); --- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS END - --- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS START - -UPDATE notification_rule -SET trigger_config = REGEXP_REPLACE( - trigger_config, - '"CASSANDRA_QUERIES"', - '"CASSANDRA_WRITE_QUERIES_CORE","CASSANDRA_READ_QUERIES_CORE","CASSANDRA_WRITE_QUERIES_RULE_ENGINE","CASSANDRA_READ_QUERIES_RULE_ENGINE","CASSANDRA_WRITE_QUERIES_MONOLITH","CASSANDRA_READ_QUERIES_MONOLITH"', - 'g' - ) -WHERE trigger_type = 'RATE_LIMITS' - AND trigger_config LIKE '%"CASSANDRA_QUERIES"%'; - --- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS END +-- UPDATE OTA PACKAGE EXTERNAL ID END diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index db7e37b368..ee9f9e3cea 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -67,7 +67,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS protected static final List SUPPORTED_ENTITY_TYPES = List.of( EntityType.CUSTOMER, EntityType.RULE_CHAIN, EntityType.TB_RESOURCE, EntityType.DASHBOARD, EntityType.ASSET_PROFILE, EntityType.ASSET, - EntityType.DEVICE_PROFILE, EntityType.DEVICE, + EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE, EntityType.DEVICE, EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE ); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java index 7d5f7ee57e..2f5355f637 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java @@ -38,6 +38,8 @@ public class DeviceExportService extends BaseEntityExportService ctx, Device device, DeviceExportData exportData) { device.setCustomerId(getExternalIdOrElseInternal(ctx, device.getCustomerId())); device.setDeviceProfileId(getExternalIdOrElseInternal(ctx, device.getDeviceProfileId())); + device.setFirmwareId(getExternalIdOrElseInternal(ctx, device.getFirmwareId())); + device.setSoftwareId(getExternalIdOrElseInternal(ctx, device.getSoftwareId())); if (ctx.getSettings().isExportCredentials()) { var credentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(ctx.getTenantId(), device.getId()); credentials.setId(null); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java index 6c212f0684..6a98bd2a7c 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java @@ -34,6 +34,8 @@ public class DeviceProfileExportService extends BaseEntityExportService> { + + @Override + protected void setRelatedEntities(EntitiesExportCtx ctx, OtaPackage otaPackage, EntityExportData exportData) { + otaPackage.setDeviceProfileId(getExternalIdOrElseInternal(ctx, otaPackage.getDeviceProfileId())); + } + + @Override + public Set getSupportedEntityTypes() { + return Set.of(EntityType.OTA_PACKAGE); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index 57b4737be6..92fdcb09c4 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -71,7 +71,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -148,6 +147,7 @@ public abstract class BaseEntityImportService importResult, D exportData, IdProvider idProvider) throws ThingsboardException { E savedEntity = importResult.getSavedEntity(); E oldEntity = importResult.getOldEntity(); @@ -405,7 +404,9 @@ public abstract class BaseEntityImportService ID getInternalId(ID externalId, boolean throwExceptionIfNotFound) { - if (externalId == null || externalId.isNullUid()) return null; + if (externalId == null || externalId.isNullUid()) { + return null; + } if (EntityType.TENANT.equals(externalId.getEntityType())) { return (ID) ctx.getTenantId(); @@ -432,7 +433,9 @@ public abstract class BaseEntityImportService getInternalIdByUuid(UUID externalUuid, boolean fetchAllUUIDs, Set hints) { - if (externalUuid.equals(EntityId.NULL_UUID)) return Optional.empty(); + if (externalUuid.equals(EntityId.NULL_UUID)) { + return Optional.empty(); + } for (EntityType entityType : EntityType.values()) { Optional externalId = buildEntityId(entityType, externalUuid); @@ -483,10 +486,6 @@ public abstract class BaseEntityImportService T getOldEntityField(O oldEntity, Function getter) { - return oldEntity == null ? null : getter.apply(oldEntity); - } - protected void replaceIdsRecursively(EntitiesImportCtx ctx, IdProvider idProvider, JsonNode json, Set skippedRootFields, Pattern includedFieldsPattern, LinkedHashSet hints) { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java index 4ace9ff938..0cbb471b6f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java @@ -44,8 +44,8 @@ public class DeviceImportService extends BaseEntityImportService> { + + private final OtaPackageService otaPackageService; + + @Override + protected void setOwner(TenantId tenantId, OtaPackage otaPackage, IdProvider idProvider) { + otaPackage.setTenantId(tenantId); + } + + @Override + protected OtaPackage prepare(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackage oldOtaPackage, EntityExportData exportData, IdProvider idProvider) { + otaPackage.setDeviceProfileId(idProvider.getInternalId(otaPackage.getDeviceProfileId())); + return otaPackage; + } + + @Override + protected OtaPackage findExistingEntity(EntitiesImportCtx ctx, OtaPackage otaPackage, IdProvider idProvider) { + OtaPackage existingOtaPackage = super.findExistingEntity(ctx, otaPackage, idProvider); + if (existingOtaPackage == null && ctx.isFindExistingByName()) { + existingOtaPackage = otaPackageService.findOtaPackageByTenantIdAndTitle(ctx.getTenantId(), otaPackage.getType(), otaPackage.getTitle()); + } + return existingOtaPackage; + } + + @Override + protected OtaPackage deepCopy(OtaPackage otaPackage) { + return new OtaPackage(otaPackage); + } + + @Override + protected void cleanupForComparison(OtaPackage otaPackage) { + super.cleanupForComparison(otaPackage); + } + + @Override + protected OtaPackage saveOrUpdate(EntitiesImportCtx ctx, OtaPackage otaPackage, EntityExportData exportData, IdProvider idProvider, CompareResult compareResult) { + return otaPackageService.saveOtaPackage(otaPackage); + } + + @Override + protected void onEntitySaved(User user, OtaPackage savedOtaPackage, OtaPackage oldOtaPackage) throws ThingsboardException { + super.onEntitySaved(user, savedOtaPackage, oldOtaPackage); + } + + @Override + public EntityType getEntityType() { + return EntityType.OTA_PACKAGE; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index da3b214afb..e70a0cd37c 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -66,6 +66,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; @@ -203,11 +204,12 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { AssetProfile assetProfile = createAssetProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Asset profile 1"); Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1"); DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1"); - Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1"); + OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); + Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1", firmware.getId(), null); CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), asset.getId()); Map entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(), - ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId()) + ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId(), firmware.getId()) .map(entityId -> { try { return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder() @@ -275,12 +277,17 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { verify(tbClusterService).sendNotificationMsgToEdge(any(), any(), eq(importedDeviceProfile.getId()), any(), any(), eq(EdgeEventActionType.ADDED), any()); verify(otaPackageStateService).update(eq(importedDeviceProfile), eq(false), eq(false)); + OtaPackage importedFirmware = (OtaPackage) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.OTA_PACKAGE)).getSavedEntity(); + verify(entityActionService).logEntityAction(any(), eq(importedFirmware.getId()), eq(importedFirmware), + any(), eq(ActionType.ADDED), isNull()); + Device importedDevice = (Device) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE)).getSavedEntity(); verify(entityActionService).logEntityAction(any(), eq(importedDevice.getId()), eq(importedDevice), any(), eq(ActionType.ADDED), isNull()); verify(tbClusterService).onDeviceUpdated(eq(importedDevice), isNull()); importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE)); verify(tbClusterService, Mockito.never()).onDeviceUpdated(eq(importedDevice), eq(importedDevice)); + assertThat(importedDevice.getFirmwareId()).isEqualTo(importedFirmware.getId()); // calculated field of imported device: List calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId()); @@ -318,14 +325,15 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { assetProfile = assetProfileService.saveAssetProfile(assetProfile); DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1"); - Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1"); + OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); + Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1", firmware.getId(), null); EntityView entityView = createEntityView(tenantId1, customer.getId(), device.getId(), "Entity view 1"); CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), device.getId()); Map ids = new HashMap<>(); for (EntityId entityId : List.of(customer.getId(), ruleChain.getId(), dashboard.getId(), assetProfile.getId(), asset.getId(), - deviceProfile.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) { + deviceProfile.getId(), firmware.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) { EntityExportData exportData = exportEntity(getSecurityUser(tenantAdmin1), entityId); EntityImportResult importResult = importEntity(getSecurityUser(tenantAdmin2), exportData, EntityImportSettings.builder() .saveCredentials(false) @@ -359,12 +367,17 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { assertThat(exportedDeviceProfile.getDefaultRuleChainId()).isEqualTo(ruleChain.getId()); assertThat(exportedDeviceProfile.getDefaultDashboardId()).isEqualTo(dashboard.getId()); - EntityExportData entityExportData = exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId())); + OtaPackage exportedFirmware = (OtaPackage) exportEntity(tenantAdmin2, (OtaPackageId) ids.get(firmware.getId())).getEntity(); + assertThat(exportedFirmware.getDeviceProfileId()).isEqualTo(exportedDeviceProfile.getId()); + assertThat(exportedFirmware.getId()).isEqualTo(firmware.getId()); + + EntityExportData entityExportData = exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId())); Device exportedDevice = entityExportData.getEntity(); assertThat(exportedDevice.getCustomerId()).isEqualTo(customer.getId()); assertThat(exportedDevice.getDeviceProfileId()).isEqualTo(deviceProfile.getId()); + assertThat(exportedDevice.getFirmwareId()).isEqualTo(firmware.getId()); - List calculatedFields = ((DeviceExportData) entityExportData).getCalculatedFields(); + List calculatedFields = entityExportData.getCalculatedFields(); assertThat(calculatedFields.size()).isOne(); CalculatedField field = calculatedFields.get(0); assertThat(field.getName()).isEqualTo(calculatedField.getName()); @@ -380,13 +393,15 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { deviceProfileService.saveDeviceProfile(importedDeviceProfile); } - protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name) { + protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name, OtaPackageId firmwareId, OtaPackageId softwareId) { Device device = new Device(); device.setTenantId(tenantId); device.setCustomerId(customerId); device.setName(name); device.setLabel("lbl"); device.setDeviceProfileId(deviceProfileId); + device.setFirmwareId(firmwareId); + device.setSoftwareId(softwareId); DeviceData deviceData = new DeviceData(); deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); device.setDeviceData(deviceData); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index c7d4068210..5c5b7bb107 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -116,8 +116,8 @@ import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.controller.TbResourceControllerTest.TEST_DATA; import static org.thingsboard.server.controller.TbResourceControllerTest.JS_TEST_FILE_NAME; +import static org.thingsboard.server.controller.TbResourceControllerTest.TEST_DATA; @DaoSqlTest public class VersionControlTest extends AbstractControllerTest { @@ -262,19 +262,24 @@ public class VersionControlTest extends AbstractControllerTest { } @Test - public void testDeviceVc_withProfile_betweenTenants() throws Exception { + public void testDeviceVc_withProfileAndOtaPackage_betweenTenants() throws Exception { DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile of tenant 1"); createVersion("profiles", EntityType.DEVICE_PROFILE); - Device device = createDevice(null, deviceProfile.getId(), "Device of tenant 1", "test1"); - String versionId = createVersion("devices", EntityType.DEVICE); + OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); + OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE); + Device device = createDevice(null, deviceProfile.getId(), "Device of tenant 1", "test1", newDevice -> { + newDevice.setFirmwareId(firmware.getId()); + newDevice.setSoftwareId(software.getId()); + }); + String versionId = createVersion("devices with ota", EntityType.DEVICE, EntityType.OTA_PACKAGE); DeviceCredentials deviceCredentials = findDeviceCredentials(device.getId()); DeviceCredentials newCredentials = new DeviceCredentials(deviceCredentials); newCredentials.setCredentialsId("new access token"); // updating access token to avoid constraint errors on import doPost("/api/device/credentials", newCredentials, DeviceCredentials.class); - assertThat(listVersions()).extracting(EntityVersion::getName).containsExactly("devices", "profiles"); + assertThat(listVersions()).extracting(EntityVersion::getName).containsExactly("devices with ota", "profiles"); loginTenant2(); - Map result = loadVersion(versionId, EntityType.DEVICE, EntityType.DEVICE_PROFILE); + Map result = loadVersion(versionId, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE); assertThat(result.get(EntityType.DEVICE).getCreated()).isEqualTo(1); assertThat(result.get(EntityType.DEVICE_PROFILE).getCreated()).isEqualTo(1); @@ -293,6 +298,13 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedCredentials.getCredentialsId()).isEqualTo(deviceCredentials.getCredentialsId()); assertThat(importedCredentials.getCredentialsValue()).isEqualTo(deviceCredentials.getCredentialsValue()); assertThat(importedCredentials.getCredentialsType()).isEqualTo(deviceCredentials.getCredentialsType()); + + OtaPackage importedFirmwareOta = findOtaPackage(firmware.getTitle()); + OtaPackage importedSoftwareOta = findOtaPackage(software.getTitle()); + checkImportedEntity(tenantId1, firmware, tenantId2, importedFirmwareOta); + checkImportedOtaPackageData(firmware, importedFirmwareOta); + checkImportedEntity(tenantId1, software, tenantId2, importedSoftwareOta); + checkImportedOtaPackageData(software, importedSoftwareOta); } @Test @@ -653,6 +665,52 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedCalculatedField.getType()).isEqualTo(calculatedField.getType()); } + @Test + public void testOtaPackageVc_sameTenant() throws Exception { + DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile v1.0"); + OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); + OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE); + String versionId = createVersion("ota packages", EntityType.OTA_PACKAGE); + + OtaPackage firmwareOta = findOtaPackage(firmware.getTitle()); + OtaPackage softwareOta = findOtaPackage(software.getTitle()); + + loadVersion(versionId, EntityType.OTA_PACKAGE); + OtaPackage importedFirmwareOta = findOtaPackage(firmwareOta.getTitle()); + OtaPackage importedSoftwareOta = findOtaPackage(softwareOta.getTitle()); + checkImportedEntity(tenantId1, firmwareOta, tenantId1, importedFirmwareOta); + checkImportedOtaPackageData(firmwareOta, importedFirmwareOta); + checkImportedEntity(tenantId1, softwareOta, tenantId1, importedSoftwareOta); + checkImportedOtaPackageData(softwareOta, importedSoftwareOta); + } + + @Test + public void testOtaPackageVc_betweenTenants() throws Exception { + DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile v1.0"); + OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); + OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE); + String versionId = createVersion("ota packages", EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE); + + OtaPackage firmwareOta = findOtaPackage(firmware.getTitle()); + OtaPackage softwareOta = findOtaPackage(software.getTitle()); + + loginTenant2(); + loadVersion(versionId, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE); + OtaPackage importedFirmwareOta = findOtaPackage(firmwareOta.getTitle()); + OtaPackage importedSoftwareOta = findOtaPackage(softwareOta.getTitle()); + checkImportedEntity(tenantId1, firmwareOta, tenantId2, importedFirmwareOta); + checkImportedOtaPackageData(firmwareOta, importedFirmwareOta); + checkImportedEntity(tenantId1, softwareOta, tenantId2, importedSoftwareOta); + checkImportedOtaPackageData(softwareOta, importedSoftwareOta); + } + + protected void checkImportedOtaPackageData(OtaPackage otaPackage, OtaPackage importedOtaPackage) { + assertThat(importedOtaPackage.getName()).isEqualTo(otaPackage.getName()); + assertThat(importedOtaPackage.getTag()).isEqualTo(otaPackage.getTag()); + assertThat(importedOtaPackage.getType()).isEqualTo(otaPackage.getType()); + assertThat(importedOtaPackage.getFileName()).isEqualTo(otaPackage.getFileName()); + } + @Test public void testResourceVc_sameTenant() throws Exception { TbResourceInfo resourceInfo = createResource("Test resource"); @@ -923,6 +981,7 @@ public class VersionControlTest extends AbstractControllerTest { otaPackage.setDeviceProfileId(deviceProfileId); otaPackage.setType(type); otaPackage.setTitle("My " + type); + otaPackage.setTag("My " + type); otaPackage.setVersion("v1.0"); otaPackage.setFileName("filename.txt"); otaPackage.setContentType("text/plain"); @@ -933,6 +992,10 @@ public class VersionControlTest extends AbstractControllerTest { return otaPackageService.saveOtaPackage(otaPackage); } + private OtaPackage findOtaPackage(String title) throws Exception { + return doGetTypedWithPageLink("/api/otaPackages?", new TypeReference>() {}, new PageLink(100, 0, title)).getData().get(0); + } + protected Dashboard createDashboard(CustomerId customerId, String name) { Dashboard dashboard = new Dashboard(); dashboard.setTitle(name); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java index 12c8e5053b..561b807b2b 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java @@ -41,6 +41,8 @@ public interface OtaPackageService extends EntityDaoService { OtaPackageInfo findOtaPackageInfoById(TenantId tenantId, OtaPackageId otaPackageId); + OtaPackage findOtaPackageByTenantIdAndTitle(TenantId tenantId, OtaPackageType type, String title); + ListenableFuture findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId); PageData findTenantOtaPackagesByTenantId(TenantId tenantId, PageLink pageLink); @@ -52,4 +54,5 @@ public interface OtaPackageService extends EntityDaoService { void deleteOtaPackagesByTenantId(TenantId tenantId); long sumDataSizeByTenantId(TenantId tenantId); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java index 26376bbda3..9b4609dfef 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java @@ -20,6 +20,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.OtaPackageId; +import java.io.Serial; import java.nio.ByteBuffer; @Schema @@ -27,6 +28,7 @@ import java.nio.ByteBuffer; @EqualsAndHashCode(callSuper = true) public class OtaPackage extends OtaPackageInfo { + @Serial private static final long serialVersionUID = 3091601761339422546L; @Schema(description = "OTA Package data.", accessMode = Schema.AccessMode.READ_ONLY) @@ -44,4 +46,5 @@ public class OtaPackage extends OtaPackageInfo { super(otaPackage); this.data = otaPackage.getData(); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java index dfdbd83e80..138e7690e3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java @@ -15,7 +15,9 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -29,12 +31,15 @@ import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +import java.io.Serial; + @Schema @Slf4j @Data @EqualsAndHashCode(callSuper = true) -public class OtaPackageInfo extends BaseDataWithAdditionalInfo implements HasName, HasTenantId, HasTitle { +public class OtaPackageInfo extends BaseDataWithAdditionalInfo implements HasName, HasTenantId, HasTitle, ExportableEntity { + @Serial private static final long serialVersionUID = 3168391583570815419L; @Schema(description = "JSON object with Tenant Id. Tenant Id of the ota package can't be changed.", accessMode = Schema.AccessMode.READ_ONLY) @@ -77,6 +82,8 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp @Schema(description = "OTA Package data size.", example = "8", accessMode = Schema.AccessMode.READ_ONLY) private Long dataSize; + private OtaPackageId externalId; + public OtaPackageInfo() { super(); } @@ -100,6 +107,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp this.checksumAlgorithm = otaPackageInfo.getChecksumAlgorithm(); this.checksum = otaPackageInfo.getChecksum(); this.dataSize = otaPackageInfo.getDataSize(); + this.externalId = otaPackageInfo.getExternalId(); } @Schema(description = "JSON object with the ota package Id. " + @@ -128,9 +136,21 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp return StringUtils.isNotEmpty(url); } + @JsonProperty("otaVersion") + @JsonAlias("version") + public void setVersion(String version) { + this.version = version; + } + + @JsonProperty("otaVersion") + public String getVersion() { + return version; + } + @Schema(description = "OTA Package description.", example = "Description for the OTA Package fw_1.0") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java index 498fa5be3e..ba37067106 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java @@ -23,6 +23,7 @@ import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.TbResourceId; +import java.io.Serial; import java.util.Base64; import java.util.Optional; @@ -31,6 +32,7 @@ import java.util.Optional; @EqualsAndHashCode(callSuper = true) public class TbResource extends TbResourceInfo { + @Serial private static final long serialVersionUID = 7379609705527272306L; private byte[] data; @@ -88,4 +90,5 @@ public class TbResource extends TbResourceInfo { public String toString() { return super.toString(); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java index c763daeb7f..9d7187f7f4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.Dashboard; 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.OtaPackage; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; @@ -58,7 +59,8 @@ import java.lang.annotation.Target; @Type(name = "NOTIFICATION_TEMPLATE", value = NotificationTemplate.class), @Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class), @Type(name = "NOTIFICATION_RULE", value = NotificationRule.class), - @Type(name = "TB_RESOURCE", value = TbResource.class) + @Type(name = "TB_RESOURCE", value = TbResource.class), + @Type(name = "OTA_PACKAGE", value = OtaPackage.class) }) @JsonIgnoreProperties(value = {"tenantId", "createdTime", "version"}, ignoreUnknown = true) public @interface JsonTbEntity { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java index 1ffafd305e..6ee4bdd89f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java @@ -38,6 +38,7 @@ import org.thingsboard.server.dao.util.mapping.JsonConverter; import java.nio.ByteBuffer; import java.util.UUID; +import static org.thingsboard.server.dao.model.ModelConstants.EXTERNAL_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_CHECKSUM_ALGORITHM_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_CHECKSUM_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_CONTENT_TYPE_COLUMN; @@ -105,6 +106,9 @@ public class OtaPackageEntity extends BaseSqlEntity { @Column(name = ModelConstants.OTA_PACKAGE_ADDITIONAL_INFO_COLUMN) private JsonNode additionalInfo; + @Column(name = EXTERNAL_ID_PROPERTY) + private UUID externalId; + public OtaPackageEntity() { super(); } @@ -128,6 +132,7 @@ public class OtaPackageEntity extends BaseSqlEntity { this.data = otaPackage.getData().array(); this.dataSize = otaPackage.getDataSize(); this.additionalInfo = otaPackage.getAdditionalInfo(); + this.externalId = getUuid(otaPackage.getExternalId()); } @Override @@ -153,6 +158,8 @@ public class OtaPackageEntity extends BaseSqlEntity { otaPackage.setHasData(true); } otaPackage.setAdditionalInfo(additionalInfo); + otaPackage.setExternalId(getEntityId(externalId, OtaPackageId::new)); return otaPackage; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java index c1e83cf511..a1625f2595 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java @@ -100,6 +100,9 @@ public class OtaPackageInfoEntity extends BaseSqlEntity { @Column(name = ModelConstants.OTA_PACKAGE_ADDITIONAL_INFO_COLUMN) private JsonNode additionalInfo; + @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) + private UUID externalId; + @Transient private boolean hasData; @@ -125,11 +128,12 @@ public class OtaPackageInfoEntity extends BaseSqlEntity { this.checksum = otaPackageInfo.getChecksum(); this.dataSize = otaPackageInfo.getDataSize(); this.additionalInfo = otaPackageInfo.getAdditionalInfo(); + this.externalId = getUuid(otaPackageInfo.getExternalId()); } public OtaPackageInfoEntity(UUID id, long createdTime, UUID tenantId, UUID deviceProfileId, OtaPackageType type, String title, String version, String tag, String url, String fileName, String contentType, ChecksumAlgorithm checksumAlgorithm, String checksum, Long dataSize, - Object additionalInfo, boolean hasData) { + Object additionalInfo, UUID externalId, boolean hasData) { this.id = id; this.createdTime = createdTime; this.tenantId = tenantId; @@ -146,6 +150,7 @@ public class OtaPackageInfoEntity extends BaseSqlEntity { this.dataSize = dataSize; this.hasData = hasData; this.additionalInfo = JacksonUtil.convertValue(additionalInfo, JsonNode.class); + this.externalId = externalId; } @Override @@ -168,6 +173,8 @@ public class OtaPackageInfoEntity extends BaseSqlEntity { otaPackageInfo.setDataSize(dataSize); otaPackageInfo.setAdditionalInfo(additionalInfo); otaPackageInfo.setHasData(hasData); + otaPackageInfo.setExternalId(getEntityId(externalId, OtaPackageId::new)); return otaPackageInfo; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 16d8517b6d..bf52de037f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -54,6 +54,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink; @Slf4j @RequiredArgsConstructor public class BaseOtaPackageService extends AbstractCachedEntityService implements OtaPackageService { + public static final String INCORRECT_OTA_PACKAGE_ID = "Incorrect otaPackageId "; public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; @@ -73,7 +74,7 @@ public class BaseOtaPackageService extends AbstractCachedEntityService Hashing.md5(); + case SHA256 -> Hashing.sha256(); + case SHA384 -> Hashing.sha384(); + case SHA512 -> Hashing.sha512(); + case CRC32 -> Hashing.crc32(); + case MURMUR3_32 -> Hashing.murmur3_32(); + case MURMUR3_128 -> Hashing.murmur3_128(); + default -> throw new DataValidationException("Unknown checksum algorithm!"); + }; } @Override @@ -171,6 +164,12 @@ public class BaseOtaPackageService extends AbstractCachedEntityService otaPackageInfoDao.findById(tenantId, otaPackageId.getId()), true); } + @Override + public OtaPackage findOtaPackageByTenantIdAndTitle(TenantId tenantId, OtaPackageType type, String title) { + log.trace("Executing findOtaPackageByTenantIdAndTitle [{}] [{}] [{}]", tenantId, type, title); + return otaPackageDao.findOtaPackageByTenantIdAndTitle(tenantId, type, title); + } + @Override public ListenableFuture findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId) { log.trace("Executing findOtaPackageInfoByIdAsync [{}]", otaPackageId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java index f8f877e55e..de81f4e72d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java @@ -16,12 +16,17 @@ package org.thingsboard.server.dao.ota; import org.thingsboard.server.common.data.OtaPackage; +import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.TenantEntityWithDataDao; -public interface OtaPackageDao extends Dao, TenantEntityWithDataDao { +public interface OtaPackageDao extends Dao, TenantEntityWithDataDao, ExportableEntityDao { Long sumDataSizeByTenantId(TenantId tenantId); + OtaPackage findOtaPackageByTenantIdAndTitle(TenantId tenantId, OtaPackageType type, String title); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java index 780f67932e..da96b83d28 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java @@ -22,7 +22,9 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackage; +import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; @@ -43,24 +45,52 @@ public class JpaOtaPackageDao extends JpaAbstractDao getEntityClass() { - return OtaPackageEntity.class; + public Long sumDataSizeByTenantId(TenantId tenantId) { + return otaPackageRepository.sumDataSizeByTenantId(tenantId.getId()); } + @Transactional @Override - protected JpaRepository getRepository() { - return otaPackageRepository; + public OtaPackage findOtaPackageByTenantIdAndTitle(TenantId tenantId, OtaPackageType type, String title) { + return DaoUtil.getData(otaPackageRepository.findByTenantIdAndTypeAndTitle(tenantId.getId(), type, title)); } + @Transactional @Override - public Long sumDataSizeByTenantId(TenantId tenantId) { - return otaPackageRepository.sumDataSizeByTenantId(tenantId.getId()); + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(otaPackageRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); } @Transactional @Override - public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { - return DaoUtil.toPageData(otaPackageRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + public PageData findByTenantId(UUID tenantId, PageLink pageLink) { + return findAllByTenantId(TenantId.fromUUID(tenantId), pageLink); + } + + @Override + public PageData findIdsByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.pageToPageData(otaPackageRepository.findIdsByTenantId(tenantId, DaoUtil.toPageable(pageLink)).map(OtaPackageId::new)); + } + + @Transactional + @Override + public OtaPackage findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { + return DaoUtil.getData(otaPackageRepository.findByTenantIdAndExternalId(tenantId, externalId)); + } + + @Override + public OtaPackageId getExternalIdByInternal(OtaPackageId internalId) { + return DaoUtil.toEntityId(otaPackageRepository.getExternalIdById(internalId.getId()), OtaPackageId::new); + } + + @Override + protected Class getEntityClass() { + return OtaPackageEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return otaPackageRepository; } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java index fe7e2bf015..c5ca894a00 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java @@ -26,14 +26,15 @@ import org.thingsboard.server.dao.model.sql.OtaPackageInfoEntity; import java.util.UUID; public interface OtaPackageInfoRepository extends JpaRepository { - @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE " + + + @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, f.externalId, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE " + "f.tenantId = :tenantId " + "AND (:searchText IS NULL OR ilike(f.title, CONCAT('%', :searchText, '%')) = true)") Page findAllByTenantId(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, Pageable pageable); - @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, true) FROM OtaPackageEntity f WHERE " + + @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, f.externalId, true) FROM OtaPackageEntity f WHERE " + "f.tenantId = :tenantId " + "AND f.deviceProfileId = :deviceProfileId " + "AND f.type = :type " + @@ -45,7 +46,7 @@ public interface OtaPackageInfoRepository extends JpaRepository { +public interface OtaPackageRepository extends JpaRepository, ExportableEntityRepository { @Query(value = "SELECT COALESCE(SUM(ota.data_size), 0) FROM ota_package ota WHERE ota.tenant_id = :tenantId AND ota.data IS NOT NULL", nativeQuery = true) Long sumDataSizeByTenantId(@Param("tenantId") UUID tenantId); Page findByTenantId(UUID tenantId, Pageable pageable); + OtaPackageEntity findByTenantIdAndTypeAndTitle(UUID tenantId, OtaPackageType type, String title); + + @Query("SELECT externalId FROM OtaPackageEntity WHERE id = :id") + UUID getExternalIdById(@Param("id") UUID id); + + @Query("SELECT r.id FROM OtaPackageEntity r WHERE r.tenantId = :tenantId") + Page findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + } diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index ad311f00df..0a6d6e578d 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -91,6 +91,8 @@ CREATE INDEX IF NOT EXISTS idx_widgets_bundle_external_id ON widgets_bundle(tena CREATE INDEX IF NOT EXISTS idx_rule_node_external_id ON rule_node(rule_chain_id, external_id); +CREATE INDEX IF NOT EXISTS idx_ota_package_external_id ON ota_package(tenant_id, external_id); + CREATE INDEX IF NOT EXISTS idx_rule_node_type_id_configuration_version ON rule_node(type, id, configuration_version); CREATE INDEX IF NOT EXISTS idx_api_usage_state_entity_id ON api_usage_state(entity_id); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index f2a0bc26c1..2cc8cbad0e 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -216,7 +216,9 @@ CREATE TABLE IF NOT EXISTS ota_package ( data oid, data_size bigint, additional_info varchar, - CONSTRAINT ota_package_tenant_title_version_unq_key UNIQUE (tenant_id, title, version) + external_id uuid, + CONSTRAINT ota_package_tenant_title_version_unq_key UNIQUE (tenant_id, title, version), + CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id) ); CREATE TABLE IF NOT EXISTS queue ( From 7ffd468d0684b69a566acc1119053475e5466649 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 11 Jun 2025 15:13:59 +0300 Subject: [PATCH 048/435] Improve OtaPackageExportData --- .../impl/OtaPackageExportService.java | 11 ++++-- .../impl/OtaPackageImportService.java | 15 ++------ .../server/common/data/OtaPackageInfo.java | 13 ------- .../common/data/sync/ie/EntityExportData.java | 3 +- .../data/sync/ie/OtaPackageExportData.java | 37 +++++++++++++++++++ .../thingsboard/common/util/JacksonUtil.java | 3 -- .../server/dao/sql/JpaAbstractDao.java | 3 -- .../server/dao/sql/asset/JpaAssetDao.java | 3 -- 8 files changed, 51 insertions(+), 37 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java index f9285e3699..1cfc70e39f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java @@ -20,7 +20,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.id.OtaPackageId; -import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.common.data.sync.ie.OtaPackageExportData; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; @@ -29,13 +29,18 @@ import java.util.Set; @Service @TbCoreComponent @RequiredArgsConstructor -public class OtaPackageExportService extends BaseEntityExportService> { +public class OtaPackageExportService extends BaseEntityExportService { @Override - protected void setRelatedEntities(EntitiesExportCtx ctx, OtaPackage otaPackage, EntityExportData exportData) { + protected void setRelatedEntities(EntitiesExportCtx ctx, OtaPackage otaPackage, OtaPackageExportData exportData) { otaPackage.setDeviceProfileId(getExternalIdOrElseInternal(ctx, otaPackage.getDeviceProfileId())); } + @Override + protected OtaPackageExportData newExportData() { + return new OtaPackageExportData(); + } + @Override public Set getSupportedEntityTypes() { return Set.of(EntityType.OTA_PACKAGE); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java index b5c583e94d..d181a9bf0a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java @@ -19,11 +19,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackage; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.common.data.sync.ie.OtaPackageExportData; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; @@ -31,7 +29,7 @@ import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; @Service @TbCoreComponent @RequiredArgsConstructor -public class OtaPackageImportService extends BaseEntityImportService> { +public class OtaPackageImportService extends BaseEntityImportService { private final OtaPackageService otaPackageService; @@ -41,7 +39,7 @@ public class OtaPackageImportService extends BaseEntityImportService exportData, IdProvider idProvider) { + protected OtaPackage prepare(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackage oldOtaPackage, OtaPackageExportData exportData, IdProvider idProvider) { otaPackage.setDeviceProfileId(idProvider.getInternalId(otaPackage.getDeviceProfileId())); return otaPackage; } @@ -66,15 +64,10 @@ public class OtaPackageImportService extends BaseEntityImportService exportData, IdProvider idProvider, CompareResult compareResult) { + protected OtaPackage saveOrUpdate(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackageExportData exportData, IdProvider idProvider, CompareResult compareResult) { return otaPackageService.saveOtaPackage(otaPackage); } - @Override - protected void onEntitySaved(User user, OtaPackage savedOtaPackage, OtaPackage oldOtaPackage) throws ThingsboardException { - super.onEntitySaved(user, savedOtaPackage, oldOtaPackage); - } - @Override public EntityType getEntityType() { return EntityType.OTA_PACKAGE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java index 138e7690e3..2930ff41d1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java @@ -15,9 +15,7 @@ */ package org.thingsboard.server.common.data; -import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -136,17 +134,6 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp return StringUtils.isNotEmpty(url); } - @JsonProperty("otaVersion") - @JsonAlias("version") - public void setVersion(String version) { - this.version = version; - } - - @JsonProperty("otaVersion") - public String getVersion() { - return version; - } - @Schema(description = "OTA Package description.", example = "Description for the OTA Package fw_1.0") @Override public JsonNode getAdditionalInfo() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java index a1692aef04..5e1f98638a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java @@ -41,7 +41,8 @@ import java.util.Map; @Type(name = "DEVICE", value = DeviceExportData.class), @Type(name = "RULE_CHAIN", value = RuleChainExportData.class), @Type(name = "WIDGET_TYPE", value = WidgetTypeExportData.class), - @Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class) + @Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class), + @Type(name = "OTA_PACKAGE", value = OtaPackageExportData.class) }) @JsonInclude(JsonInclude.Include.NON_NULL) @Data diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java new file mode 100644 index 0000000000..df12bb5b08 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.sync.ie; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.OtaPackage; + +@EqualsAndHashCode(callSuper = true) +public class OtaPackageExportData extends EntityExportData { + + @JsonIgnoreProperties(value = {"tenantId", "createdTime"}, ignoreUnknown = true) + @Override + public OtaPackage getEntity() { + return super.getEntity(); + } + + @JsonIgnoreProperties(value = {"tenantId", "createdTime"}, ignoreUnknown = true) + @Override + public void setEntity(OtaPackage entity) { + super.setEntity(entity); + } + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index d153501b92..cd61c7ac20 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -62,9 +62,6 @@ import java.util.function.BiFunction; import java.util.function.UnaryOperator; import java.util.regex.Pattern; -/** - * Created by Valerii Sosliuk on 5/12/2017. - */ @Slf4j public class JacksonUtil { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java index 05577b68e1..6f0380752f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java @@ -40,9 +40,6 @@ import java.util.List; import java.util.Optional; import java.util.UUID; -/** - * @author Valerii Sosliuk - */ @Slf4j @SqlDao public abstract class JpaAbstractDao, D> diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index 4e99fb57e4..4b55884792 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -48,9 +48,6 @@ import java.util.UUID; import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityInfosToDto; -/** - * Created by Valerii Sosliuk on 5/19/2017. - */ @Component @SqlDao @Slf4j From 562b23aef103092d69f1c5025ea713d257c4c8e2 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 11 Jun 2025 15:50:35 +0300 Subject: [PATCH 049/435] Minor clean up --- .../sync/ie/importing/impl/OtaPackageImportService.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java index d181a9bf0a..896724c827 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java @@ -58,11 +58,6 @@ public class OtaPackageImportService extends BaseEntityImportService Date: Wed, 11 Jun 2025 16:09:34 +0300 Subject: [PATCH 050/435] Improve findOtaByName in VC --- .../sync/ie/importing/impl/OtaPackageImportService.java | 2 +- .../org/thingsboard/server/dao/ota/OtaPackageService.java | 2 +- .../thingsboard/server/dao/ota/BaseOtaPackageService.java | 6 +++--- .../java/org/thingsboard/server/dao/ota/OtaPackageDao.java | 2 +- .../thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java | 5 ++--- .../server/dao/sql/ota/OtaPackageRepository.java | 2 +- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java index 896724c827..16b204411e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java @@ -48,7 +48,7 @@ public class OtaPackageImportService extends BaseEntityImportService findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index bf52de037f..83cb35d152 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -165,9 +165,9 @@ public class BaseOtaPackageService extends AbstractCachedEntityService, TenantEntityWithDataDao, Long sumDataSizeByTenantId(TenantId tenantId); - OtaPackage findOtaPackageByTenantIdAndTitle(TenantId tenantId, OtaPackageType type, String title); + OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java index da96b83d28..7322ea4cb6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java @@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; @@ -51,8 +50,8 @@ public class JpaOtaPackageDao extends JpaAbstractDao findByTenantId(UUID tenantId, Pageable pageable); - OtaPackageEntity findByTenantIdAndTypeAndTitle(UUID tenantId, OtaPackageType type, String title); + OtaPackageEntity findByTenantIdAndTitleAndVersion(UUID tenantId, String title, String version); @Query("SELECT externalId FROM OtaPackageEntity WHERE id = :id") UUID getExternalIdById(@Param("id") UUID id); From cb106760c150848ab5ff374bf8f1a09246a40087 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 11 Jun 2025 17:42:03 +0300 Subject: [PATCH 051/435] AI rule node: add timeout support --- .../server/service/ai/AiServiceImpl.java | 39 ++++++++++++++----- .../common/data/ai/model/AiModelConfig.java | 9 +++++ .../model/GoogleAiGeminiChatModelConfig.java | 12 ++++++ .../ai/model/MistralAiChatModelConfig.java | 12 ++++++ .../data/ai/model/OpenAiChatModelConfig.java | 12 ++++++ .../rule/engine/api/RuleEngineAiService.java | 4 ++ .../thingsboard/rule/engine/ai/TbAiNode.java | 28 +++++++++++-- .../rule/engine/ai/TbAiNodeConfiguration.java | 9 ++++- 8 files changed, 111 insertions(+), 14 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java index 60d7dca7a8..eeddb5e6d2 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java @@ -23,13 +23,16 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.RuleEngineAiService; import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.model.AiModelConfig; import org.thingsboard.server.common.data.ai.model.GoogleAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.MistralAiChatModelConfig; import org.thingsboard.server.common.data.ai.model.OpenAiChatModelConfig; +import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; import org.thingsboard.server.common.data.id.AiSettingsId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.ai.AiSettingsService; +import java.time.Duration; import java.util.NoSuchElementException; import java.util.Optional; @@ -46,37 +49,53 @@ class AiServiceImpl implements RuleEngineAiService { throw new NoSuchElementException("AI settings with ID: " + aiSettingsId + " were not found"); } var aiSettings = aiSettingsOpt.get(); + return configureChatModel(aiSettings.getProviderConfig(), aiSettings.getModelConfig()); + } - return switch (aiSettings.getProvider()) { + @Override + public ChatModel configureChatModel(AiProviderConfig providerConfig, AiModelConfig modelConfig) { + return switch (providerConfig.getProvider()) { case OPENAI -> { var modelBuilder = OpenAiChatModel.builder() - .apiKey(aiSettings.getProviderConfig().getApiKey()) - .modelName(aiSettings.getModel()); + .apiKey(providerConfig.getApiKey()) + .modelName(modelConfig.getModel()); - if (aiSettings.getModelConfig() instanceof OpenAiChatModelConfig config) { + if (modelConfig instanceof OpenAiChatModelConfig config) { modelBuilder.temperature(config.getTemperature()); + if (config.getTimeoutSeconds() != null) { + modelBuilder.timeout(Duration.ofSeconds(config.getTimeoutSeconds())); + } + modelBuilder.maxRetries(config.getMaxRetries()); } yield modelBuilder.build(); } case MISTRAL_AI -> { var modelBuilder = MistralAiChatModel.builder() - .apiKey(aiSettings.getProviderConfig().getApiKey()) - .modelName(aiSettings.getModel()); + .apiKey(providerConfig.getApiKey()) + .modelName(modelConfig.getModel()); - if (aiSettings.getModelConfig() instanceof MistralAiChatModelConfig config) { + if (modelConfig instanceof MistralAiChatModelConfig config) { modelBuilder.temperature(config.getTemperature()); + if (config.getTimeoutSeconds() != null) { + modelBuilder.timeout(Duration.ofSeconds(config.getTimeoutSeconds())); + } + modelBuilder.maxRetries(config.getMaxRetries()); } yield modelBuilder.build(); } case GOOGLE_AI_GEMINI -> { var modelBuilder = GoogleAiGeminiChatModel.builder() - .apiKey(aiSettings.getProviderConfig().getApiKey()) - .modelName(aiSettings.getModel()); + .apiKey(providerConfig.getApiKey()) + .modelName(modelConfig.getModel()); - if (aiSettings.getModelConfig() instanceof GoogleAiGeminiChatModelConfig config) { + if (modelConfig instanceof GoogleAiGeminiChatModelConfig config) { modelBuilder.temperature(config.getTemperature()); + if (config.getTimeoutSeconds() != null) { + modelBuilder.timeout(Duration.ofSeconds(config.getTimeoutSeconds())); + } + modelBuilder.maxRetries(config.getMaxRetries()); } yield modelBuilder.build(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java index 5112df3213..779b75812f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java @@ -44,4 +44,13 @@ public abstract class AiModelConfig { ) private String model; + public abstract Integer getTimeoutSeconds(); + + public abstract void setTimeoutSeconds(Integer timeoutSeconds); + + public abstract Integer getMaxRetries(); + + public abstract void setMaxRetries(Integer timeoutSeconds); + + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/GoogleAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/GoogleAiGeminiChatModelConfig.java index 761ec998cc..1d85115f31 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/GoogleAiGeminiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/GoogleAiGeminiChatModelConfig.java @@ -47,4 +47,16 @@ public final class GoogleAiGeminiChatModelConfig extends AiModelConfig { ) private Double temperature; + @Schema( + accessMode = Schema.AccessMode.READ_WRITE, + description = "Timeout (in seconds) for establishing HTTP connection" + ) + private Integer timeoutSeconds; + + @Schema( + accessMode = Schema.AccessMode.READ_WRITE, + description = "Maximum number of times to retry an LLM call upon exception (except for non-retriable ones like authentication or invalid request errors)" + ) + private Integer maxRetries; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/MistralAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/MistralAiChatModelConfig.java index 20a8cfea7c..d333eb6283 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/MistralAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/MistralAiChatModelConfig.java @@ -47,4 +47,16 @@ public final class MistralAiChatModelConfig extends AiModelConfig { ) private Double temperature; + @Schema( + accessMode = Schema.AccessMode.READ_WRITE, + description = "Timeout (in seconds) for the entire HTTP call: applied to connect, read, and write operations" + ) + private Integer timeoutSeconds; + + @Schema( + accessMode = Schema.AccessMode.READ_WRITE, + description = "Maximum number of times to retry an LLM call upon exception (except for non-retriable ones like authentication or invalid request errors)" + ) + private Integer maxRetries; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/OpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/OpenAiChatModelConfig.java index a7bc3725ef..accf5c92ec 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/OpenAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/OpenAiChatModelConfig.java @@ -47,4 +47,16 @@ public final class OpenAiChatModelConfig extends AiModelConfig { ) private Double temperature; + @Schema( + accessMode = Schema.AccessMode.READ_WRITE, + description = "Timeout (in seconds) for both establishing HTTP connection and receiving a response" + ) + private Integer timeoutSeconds; + + @Schema( + accessMode = Schema.AccessMode.READ_WRITE, + description = "Maximum number of times to retry an LLM call upon exception (except for non-retriable ones like authentication or invalid request errors)" + ) + private Integer maxRetries; + } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAiService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAiService.java index ae455d3b51..d493c5599a 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAiService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAiService.java @@ -16,6 +16,8 @@ package org.thingsboard.rule.engine.api; import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.model.AiModelConfig; +import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; import org.thingsboard.server.common.data.id.AiSettingsId; import org.thingsboard.server.common.data.id.TenantId; @@ -23,4 +25,6 @@ public interface RuleEngineAiService { ChatModel configureChatModel(TenantId tenantId, AiSettingsId aiSettingsId); + ChatModel configureChatModel(AiProviderConfig providerConfig, AiModelConfig modelConfig); + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 6b0bb3803b..7ee354d581 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -35,6 +35,9 @@ 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.AiSettings; +import org.thingsboard.server.common.data.ai.model.AiModelConfig; +import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; import org.thingsboard.server.common.data.id.AiSettingsId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -42,6 +45,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; import java.util.List; import java.util.Map; +import java.util.Optional; import static com.google.common.util.concurrent.Futures.addCallback; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; @@ -60,6 +64,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { private SystemMessage systemMessage; private PromptTemplate userPromptTemplate; private ResponseFormat responseFormat; + private int timeoutSeconds; private AiSettingsId aiSettingsId; @Override @@ -88,6 +93,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { .formatted(config.getUserPrompt()) ); + timeoutSeconds = config.getTimeoutSeconds(); aiSettingsId = config.getAiSettingsId(); } @@ -95,11 +101,11 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { if (responseFormatType == ResponseFormatType.TEXT) { return null; } - return responseFormatType == ResponseFormatType.JSON && jsonSchema != null ? Langchain4jJsonSchemaAdapter.fromJsonNode(jsonSchema) : null; + return responseFormatType == ResponseFormatType.JSON && jsonSchema != null && !jsonSchema.isNull() ? Langchain4jJsonSchemaAdapter.fromJsonNode(jsonSchema) : null; } @Override - public void onMsg(TbContext ctx, TbMsg msg) { + public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { var ackedMsg = ackIfNeeded(ctx, msg); Map variables = Map.of( @@ -114,7 +120,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { .responseFormat(responseFormat) .build(); - ChatModel chatModel = ctx.getAiService().configureChatModel(ctx.getTenantId(), aiSettingsId); + ChatModel chatModel = configureChatModel(ctx); addCallback(sendChatRequest(ctx, chatModel, chatRequest), new FutureCallback<>() { @Override @@ -134,6 +140,21 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { }, directExecutor()); } + private ChatModel configureChatModel(TbContext ctx) throws TbNodeException { + Optional aiSettingsOpt = ctx.getAiSettingsService().findAiSettingsByTenantIdAndId(ctx.getTenantId(), aiSettingsId); + if (aiSettingsOpt.isEmpty()) { + throw new TbNodeException("AI settings with ID: " + aiSettingsId + " were not found", true); + } + + AiProviderConfig providerConfig = aiSettingsOpt.get().getProviderConfig(); + AiModelConfig modelConfig = aiSettingsOpt.get().getModelConfig(); + + modelConfig.setTimeoutSeconds(timeoutSeconds); + modelConfig.setMaxRetries(0); // disable retries to respect timeout set in rule node config + + return ctx.getAiService().configureChatModel(providerConfig, modelConfig); + } + private ListenableFuture sendChatRequest(TbContext ctx, ChatModel chatModel, ChatRequest chatRequest) { return ctx.getExternalCallExecutor().submit(() -> chatModel.chat(chatRequest).aiMessage().text()); } @@ -157,6 +178,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { systemMessage = null; userPromptTemplate = null; responseFormat = null; + aiSettingsId = null; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index f9341ae982..7d2b9745ab 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; import dev.langchain4j.model.chat.request.ResponseFormatType; import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -46,10 +48,14 @@ public class TbAiNodeConfiguration implements NodeConfiguration Date: Wed, 11 Jun 2025 17:51:38 +0300 Subject: [PATCH 052/435] AI rule node: use `ObjectNode` instead of more general `JsonNode` for JSON Schema config field --- .../rule/engine/ai/Langchain4jJsonSchemaAdapter.java | 5 +++-- .../main/java/org/thingsboard/rule/engine/ai/TbAiNode.java | 5 +++-- .../thingsboard/rule/engine/ai/TbAiNodeConfiguration.java | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java index 9ad745f3ed..12988c209d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.ai; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import dev.langchain4j.model.chat.request.json.JsonArraySchema; import dev.langchain4j.model.chat.request.json.JsonBooleanSchema; import dev.langchain4j.model.chat.request.json.JsonEnumSchema; @@ -42,10 +43,10 @@ final class Langchain4jJsonSchemaAdapter { /** * Creates a Langchain4j {@link JsonSchema} from the given root JSON Schema node. * - * @param rootSchemaNode a valid JSON Schema as a Jackson {@link JsonNode} + * @param rootSchemaNode a valid JSON Schema as a Jackson {@link ObjectNode} * @return the corresponding Langchain4j {@link JsonSchema} */ - public static JsonSchema fromJsonNode(JsonNode rootSchemaNode) { + public static JsonSchema fromJsonNode(ObjectNode rootSchemaNode) { return JsonSchema.builder() .name(rootSchemaNode.get("title").textValue()) .rootElement(parse(rootSchemaNode)) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 7ee354d581..0acba685ac 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.ai; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.ListenableFuture; import dev.langchain4j.data.message.SystemMessage; @@ -97,11 +98,11 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { aiSettingsId = config.getAiSettingsId(); } - private static JsonSchema getJsonSchema(ResponseFormatType responseFormatType, JsonNode jsonSchema) { + private static JsonSchema getJsonSchema(ResponseFormatType responseFormatType, ObjectNode jsonSchema) { if (responseFormatType == ResponseFormatType.TEXT) { return null; } - return responseFormatType == ResponseFormatType.JSON && jsonSchema != null && !jsonSchema.isNull() ? Langchain4jJsonSchemaAdapter.fromJsonNode(jsonSchema) : null; + return responseFormatType == ResponseFormatType.JSON && jsonSchema != null ? Langchain4jJsonSchemaAdapter.fromJsonNode(jsonSchema) : null; } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index 7d2b9745ab..96ecde330a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -16,7 +16,7 @@ package org.thingsboard.rule.engine.ai; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import dev.langchain4j.model.chat.request.ResponseFormatType; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.Max; @@ -46,7 +46,7 @@ public class TbAiNodeConfiguration implements NodeConfiguration Date: Wed, 11 Jun 2025 18:10:01 +0300 Subject: [PATCH 053/435] AI rule node: ensure AI settings exist on rule node init --- .../java/org/thingsboard/rule/engine/ai/TbAiNode.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 0acba685ac..1477f321a8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -95,6 +95,10 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { ); timeoutSeconds = config.getTimeoutSeconds(); + + if (!aiSettingsExist(ctx, config.getAiSettingsId())) { + throw new TbNodeException("[" + ctx.getTenantId() + "] AI settings with ID: " + config.getAiSettingsId() + " were not found", true); + } aiSettingsId = config.getAiSettingsId(); } @@ -105,6 +109,10 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { return responseFormatType == ResponseFormatType.JSON && jsonSchema != null ? Langchain4jJsonSchemaAdapter.fromJsonNode(jsonSchema) : null; } + private static boolean aiSettingsExist(TbContext ctx, AiSettingsId aiSettingsId) { + return ctx.getAiSettingsService().findAiSettingsByTenantIdAndId(ctx.getTenantId(), aiSettingsId).isPresent(); + } + @Override public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { var ackedMsg = ackIfNeeded(ctx, msg); From fe9caaedd4e08ce1ae5af203385a508c52008d3d Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 11 Jun 2025 18:20:26 +0300 Subject: [PATCH 054/435] AI rule node: disable rule node on edges --- .../main/java/org/thingsboard/rule/engine/ai/TbAiNode.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 1477f321a8..41c76f589b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.ai.model.AiModelConfig; import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; import org.thingsboard.server.common.data.id.AiSettingsId; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.dao.exception.DataValidationException; @@ -58,7 +59,8 @@ import static org.thingsboard.server.dao.service.ConstraintValidator.validateFie name = "AI", nodeDescription = "Interact with AI", nodeDetails = "This node makes requests to AI based on a prompt and a input message and returns a response in a form of output message", - configClazz = TbAiNodeConfiguration.class + configClazz = TbAiNodeConfiguration.class, + ruleChainTypes = RuleChainType.CORE ) public final class TbAiNode extends TbAbstractExternalNode implements TbNode { From db5e4f8d91564efeb371fde76d14ad5822311374 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 11 Jun 2025 19:35:30 +0300 Subject: [PATCH 055/435] AI rule node: make message processing non-blocking --- .../server/dao/ai/AiSettingsService.java | 3 + .../server/dao/ai/AiSettingsServiceImpl.java | 8 +++ .../thingsboard/rule/engine/ai/TbAiNode.java | 67 +++++++++---------- 3 files changed, 44 insertions(+), 34 deletions(-) diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java index f06923b008..83a66de876 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.ai; +import com.google.common.util.concurrent.FluentFuture; import org.thingsboard.server.common.data.ai.AiSettings; import org.thingsboard.server.common.data.id.AiSettingsId; import org.thingsboard.server.common.data.id.TenantId; @@ -34,6 +35,8 @@ public interface AiSettingsService extends EntityDaoService { Optional findAiSettingsByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId); + FluentFuture> findAiSettingsByTenantIdAndIdAsync(TenantId tenantId, AiSettingsId aiSettingsId); + boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java index 15e1074b96..8950e3559f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.ai; import com.google.common.collect.Sets; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -32,6 +33,7 @@ import org.thingsboard.server.dao.entity.CachedVersionedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.sql.JpaExecutorService; import java.util.List; import java.util.Optional; @@ -45,6 +47,7 @@ class AiSettingsServiceImpl extends CachedVersionedEntityService aiSettingsValidator; + private final JpaExecutorService jpaExecutor; private final AiSettingsDao aiSettingsDao; @Override @@ -97,6 +100,11 @@ class AiSettingsServiceImpl extends CachedVersionedEntityService aiSettingsDao.findByTenantIdAndId(tenantId, aiSettingsId).orElse(null))); } + @Override + public FluentFuture> findAiSettingsByTenantIdAndIdAsync(TenantId tenantId, AiSettingsId aiSettingsId) { + return FluentFuture.from(jpaExecutor.submit(() -> findAiSettingsByTenantIdAndId(tenantId, aiSettingsId))); + } + @Override @Transactional public boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 41c76f589b..def5d03477 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -17,8 +17,8 @@ package org.thingsboard.rule.engine.ai; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.ListenableFuture; import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.ChatModel; @@ -36,7 +36,6 @@ 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.AiSettings; import org.thingsboard.server.common.data.ai.model.AiModelConfig; import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; import org.thingsboard.server.common.data.id.AiSettingsId; @@ -47,9 +46,8 @@ import org.thingsboard.server.dao.exception.DataValidationException; import java.util.List; import java.util.Map; -import java.util.Optional; +import java.util.NoSuchElementException; -import static com.google.common.util.concurrent.Futures.addCallback; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static java.util.Objects.requireNonNullElse; import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields; @@ -131,43 +129,44 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { .responseFormat(responseFormat) .build(); - ChatModel chatModel = configureChatModel(ctx); - - addCallback(sendChatRequest(ctx, chatModel, chatRequest), new FutureCallback<>() { - @Override - public void onSuccess(String response) { - if (!isValidJsonObject(response)) { - response = wrapInJsonObject(response); - } - tellSuccess(ctx, ackedMsg.transform() - .data(response) - .build()); - } - - @Override - public void onFailure(@NonNull Throwable t) { - tellFailure(ctx, ackedMsg, t); - } - }, directExecutor()); + configureChatModelAsync(ctx) + .transform(chatModel -> sendChatRequest(chatModel, chatRequest), ctx.getExternalCallExecutor()) + .addCallback(new FutureCallback<>() { + @Override + public void onSuccess(String response) { + if (!isValidJsonObject(response)) { + response = wrapInJsonObject(response); + } + tellSuccess(ctx, ackedMsg.transform() + .data(response) + .build()); + } + + @Override + public void onFailure(@NonNull Throwable t) { + tellFailure(ctx, ackedMsg, t); + } + }, directExecutor()); } - private ChatModel configureChatModel(TbContext ctx) throws TbNodeException { - Optional aiSettingsOpt = ctx.getAiSettingsService().findAiSettingsByTenantIdAndId(ctx.getTenantId(), aiSettingsId); - if (aiSettingsOpt.isEmpty()) { - throw new TbNodeException("AI settings with ID: " + aiSettingsId + " were not found", true); - } + private FluentFuture configureChatModelAsync(TbContext ctx) { + return ctx.getAiSettingsService().findAiSettingsByTenantIdAndIdAsync(ctx.getTenantId(), aiSettingsId).transform(aiSettingsOpt -> { + if (aiSettingsOpt.isEmpty()) { + throw new NoSuchElementException("AI settings with ID: " + aiSettingsId + " were not found"); + } - AiProviderConfig providerConfig = aiSettingsOpt.get().getProviderConfig(); - AiModelConfig modelConfig = aiSettingsOpt.get().getModelConfig(); + AiProviderConfig providerConfig = aiSettingsOpt.get().getProviderConfig(); + AiModelConfig modelConfig = aiSettingsOpt.get().getModelConfig(); - modelConfig.setTimeoutSeconds(timeoutSeconds); - modelConfig.setMaxRetries(0); // disable retries to respect timeout set in rule node config + modelConfig.setTimeoutSeconds(timeoutSeconds); + modelConfig.setMaxRetries(0); // disable retries to respect timeout set in rule node config - return ctx.getAiService().configureChatModel(providerConfig, modelConfig); + return ctx.getAiService().configureChatModel(providerConfig, modelConfig); + }, ctx.getDbCallbackExecutor()); } - private ListenableFuture sendChatRequest(TbContext ctx, ChatModel chatModel, ChatRequest chatRequest) { - return ctx.getExternalCallExecutor().submit(() -> chatModel.chat(chatRequest).aiMessage().text()); + private String sendChatRequest(ChatModel chatModel, ChatRequest chatRequest) { + return chatModel.chat(chatRequest).aiMessage().text(); } private static boolean isValidJsonObject(String jsonString) { From ff23fa03c0e9c257a3219997d2662194a7aac532 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 11 Jun 2025 21:17:48 +0300 Subject: [PATCH 056/435] AI rule node: add dedicated thread pool for AI requests --- .../server/actors/ActorSystemContext.java | 5 ++ .../actors/ruleChain/DefaultTbContext.java | 6 ++ .../ai/AiRequestsExecutorProperties.java | 43 +++++++++++++ .../service/ai/DefaultAiRequestsExecutor.java | 63 +++++++++++++++++++ .../src/main/resources/thingsboard.yml | 10 +++ .../rule/engine/api/AiRequestsExecutor.java | 27 ++++++++ .../rule/engine/api/TbContext.java | 2 + .../thingsboard/rule/engine/ai/TbAiNode.java | 10 ++- 8 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/ai/AiRequestsExecutorProperties.java create mode 100644 application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java create mode 100644 rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AiRequestsExecutor.java diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index e420d6a6f6..5fc7247061 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -30,6 +30,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.AiRequestsExecutor; import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MqttClientSettings; @@ -319,6 +320,10 @@ public class ActorSystemContext { @Getter private AiSettingsService aiSettingsService; + @Autowired + @Getter + private AiRequestsExecutor aiRequestsExecutor; + @Autowired @Getter private EntityViewService entityViewService; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 3aba1f5379..9df08b7b87 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -23,6 +23,7 @@ import org.bouncycastle.util.Arrays; import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.api.AiRequestsExecutor; import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MqttClientSettings; @@ -1024,6 +1025,11 @@ public class DefaultTbContext implements TbContext { return mainCtx.getAiSettingsService(); } + @Override + public AiRequestsExecutor getAiRequestsExecutor() { + return mainCtx.getAiRequestsExecutor(); + } + @Override public MqttClientSettings getMqttClientSettings() { return mainCtx.getMqttClientSettings(); diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiRequestsExecutorProperties.java b/application/src/main/java/org/thingsboard/server/service/ai/AiRequestsExecutorProperties.java new file mode 100644 index 0000000000..52a0d0d57e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ai/AiRequestsExecutorProperties.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ai; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; + +@Data +@Validated +@Configuration +@ConfigurationProperties(prefix = "actors.rule.ai-requests-thread-pool") +class AiRequestsExecutorProperties { + + @NotBlank(message = "Pool name must be not blank") + private String poolName = "ai-requests"; + + @Min(value = 1, message = "Pool size must be at least 1") + private int poolSize = 50; + + @Min(value = 1, message = "Max queue size must be at least 1") + private int maxQueueSize = 10000; + + @Min(value = 1, message = "Termination timeout must be at least 1 second") + private int terminationTimeoutSeconds = 60; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java b/application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java new file mode 100644 index 0000000000..40a5bdbb63 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ai; + +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.request.ChatRequest; +import dev.langchain4j.model.chat.response.ChatResponse; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.rule.engine.api.AiRequestsExecutor; + +import java.time.Duration; + +@Lazy +@Component +@RequiredArgsConstructor +class DefaultAiRequestsExecutor implements AiRequestsExecutor { + + private final AiRequestsExecutorProperties properties; + + private ListeningExecutorService executorService; + + @PostConstruct + private void init() { + executorService = MoreExecutors.listeningDecorator( + ThingsBoardExecutors.newLimitedTasksExecutor(properties.getPoolSize(), properties.getMaxQueueSize(), properties.getPoolName()) + ); + } + + @Override + public FluentFuture sendChatRequestAsync(ChatModel chatModel, ChatRequest chatRequest) { + return FluentFuture.from(executorService.submit(() -> chatModel.chat(chatRequest))); + } + + @PreDestroy + private void destroy() { + if (executorService != null) { + MoreExecutors.shutdownAndAwaitTermination(executorService, Duration.ofSeconds(properties.getTerminationTimeoutSeconds())); + executorService = null; + } + } + +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 54c1159003..dd41b13de0 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -463,6 +463,16 @@ actors: allow_system_sms_service: "${ACTORS_RULE_ALLOW_SYSTEM_SMS_SERVICE:true}" # Specify thread pool size for external call service external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:50}" + # Configuration for the thread pool that executes HTTP calls to AI provider APIs + ai-requests-thread-pool: + # The base name for threads + pool-name: "${ACTORS_RULE_AI_REQUESTS_THREAD_POOL_NAME:ai-requests}" + # The maximum number of concurrent HTTP requests + pool-size: "${ACTORS_RULE_AI_REQUESTS_THREAD_POOL_SIZE:50}" + # The maximum queue size for pending AI requests + max-queue-size: "${ACTORS_RULE_AI_REQUESTS_THREAD_POOL_QUEUE_SIZE:10000}" + # The maximum time in seconds to wait for active tasks to complete during graceful shutdown + termination-timeout-seconds: "${ACTORS_RULE_AI_REQUESTS_THREAD_POOL_TERMINATION_TIMEOUT_SECONDS:60}" chain: # Errors for particular actors are persisted once per specified amount of milliseconds error_persist_frequency: "${ACTORS_RULE_CHAIN_ERROR_FREQUENCY:3000}" diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AiRequestsExecutor.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AiRequestsExecutor.java new file mode 100644 index 0000000000..9fd2a000ce --- /dev/null +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AiRequestsExecutor.java @@ -0,0 +1,27 @@ +/** + * 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.rule.engine.api; + +import com.google.common.util.concurrent.FluentFuture; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.request.ChatRequest; +import dev.langchain4j.model.chat.response.ChatResponse; + +public interface AiRequestsExecutor { + + FluentFuture sendChatRequestAsync(ChatModel chatModel, ChatRequest chatRequest); + +} diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 4f9e786357..e5306c0193 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -422,6 +422,8 @@ public interface TbContext { AiSettingsService getAiSettingsService(); + AiRequestsExecutor getAiRequestsExecutor(); + // Configuration parameters for the MQTT client that is used in the MQTT node and Azure IoT hub node MqttClientSettings getMqttClientSettings(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index def5d03477..955442e79b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -26,6 +26,7 @@ import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.request.ResponseFormat; import dev.langchain4j.model.chat.request.ResponseFormatType; import dev.langchain4j.model.chat.request.json.JsonSchema; +import dev.langchain4j.model.chat.response.ChatResponse; import dev.langchain4j.model.input.PromptTemplate; import org.checkerframework.checker.nullness.qual.NonNull; import org.thingsboard.common.util.JacksonUtil; @@ -130,10 +131,11 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { .build(); configureChatModelAsync(ctx) - .transform(chatModel -> sendChatRequest(chatModel, chatRequest), ctx.getExternalCallExecutor()) + .transformAsync(chatModel -> ctx.getAiRequestsExecutor().sendChatRequestAsync(chatModel, chatRequest), directExecutor()) .addCallback(new FutureCallback<>() { @Override - public void onSuccess(String response) { + public void onSuccess(ChatResponse chatResponse) { + String response = chatResponse.aiMessage().text(); if (!isValidJsonObject(response)) { response = wrapInJsonObject(response); } @@ -165,10 +167,6 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { }, ctx.getDbCallbackExecutor()); } - private String sendChatRequest(ChatModel chatModel, ChatRequest chatRequest) { - return chatModel.chat(chatRequest).aiMessage().text(); - } - private static boolean isValidJsonObject(String jsonString) { try { JsonNode result = JacksonUtil.toJsonNode(jsonString); From fbb7d5c415b734834ba4bd9caf8031a7930f7644 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 12 Jun 2025 16:46:02 +0300 Subject: [PATCH 057/435] Minor improvement to correctly save ota if using the same device --- .../sync/ie/importing/impl/DeviceProfileImportService.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java index e66d2d1267..c867286285 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java @@ -45,8 +45,8 @@ public class DeviceProfileImportService extends BaseEntityImportService Date: Thu, 12 Jun 2025 16:47:44 +0300 Subject: [PATCH 058/435] AI rule node: encapsulate AI requests thread pool properties --- .../ai/AiRequestsExecutorProperties.java | 43 ------------------- .../service/ai/DefaultAiRequestsExecutor.java | 26 +++++++++++ 2 files changed, 26 insertions(+), 43 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/ai/AiRequestsExecutorProperties.java diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiRequestsExecutorProperties.java b/application/src/main/java/org/thingsboard/server/service/ai/AiRequestsExecutorProperties.java deleted file mode 100644 index 52a0d0d57e..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/ai/AiRequestsExecutorProperties.java +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.ai; - -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotBlank; -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; -import org.springframework.validation.annotation.Validated; - -@Data -@Validated -@Configuration -@ConfigurationProperties(prefix = "actors.rule.ai-requests-thread-pool") -class AiRequestsExecutorProperties { - - @NotBlank(message = "Pool name must be not blank") - private String poolName = "ai-requests"; - - @Min(value = 1, message = "Pool size must be at least 1") - private int poolSize = 50; - - @Min(value = 1, message = "Max queue size must be at least 1") - private int maxQueueSize = 10000; - - @Min(value = 1, message = "Termination timeout must be at least 1 second") - private int terminationTimeoutSeconds = 60; - -} diff --git a/application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java b/application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java index 40a5bdbb63..ae0663eefc 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java @@ -23,9 +23,15 @@ import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.response.ChatResponse; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.rule.engine.api.AiRequestsExecutor; @@ -38,6 +44,26 @@ class DefaultAiRequestsExecutor implements AiRequestsExecutor { private final AiRequestsExecutorProperties properties; + @Data + @Validated + @Configuration + @ConfigurationProperties(prefix = "actors.rule.ai-requests-thread-pool") + private static class AiRequestsExecutorProperties { + + @NotBlank(message = "Pool name must be not blank") + private String poolName = "ai-requests"; + + @Min(value = 1, message = "Pool size must be at least 1") + private int poolSize = 50; + + @Min(value = 1, message = "Max queue size must be at least 1") + private int maxQueueSize = 10000; + + @Min(value = 1, message = "Termination timeout must be at least 1 second") + private int terminationTimeoutSeconds = 60; + + } + private ListeningExecutorService executorService; @PostConstruct From d2d22a44c21b504cc681e6e88710c8664485e50f Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 17 Jun 2025 18:17:38 +0300 Subject: [PATCH 059/435] AI rule node: add support for all data/metadata patterns in rule nodes --- .../server/common/msg/TbMsgMetaData.java | 20 ++- .../rule/engine/api/util/TbNodeUtils.java | 43 ++--- .../rule/engine/api/util/TbNodeUtilsTest.java | 158 ++++++++++++++++++ 3 files changed, 191 insertions(+), 30 deletions(-) diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java index e703183883..ef083ebccb 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java @@ -23,9 +23,6 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -/** - * Created by ashvayka on 13.01.18. - */ @Data public final class TbMsgMetaData implements Serializable { @@ -34,7 +31,7 @@ public final class TbMsgMetaData implements Serializable { private final Map data; public TbMsgMetaData() { - this.data = new ConcurrentHashMap<>(); + data = new ConcurrentHashMap<>(); } public TbMsgMetaData(Map data) { @@ -46,24 +43,29 @@ public final class TbMsgMetaData implements Serializable { * Internal constructor to create immutable TbMsgMetaData.EMPTY * */ private TbMsgMetaData(int ignored) { - this.data = Collections.emptyMap(); + data = Collections.emptyMap(); } public String getValue(String key) { - return this.data.get(key); + return data.get(key); } public void putValue(String key, String value) { if (key != null && value != null) { - this.data.put(key, value); + data.put(key, value); } } public Map values() { - return new HashMap<>(this.data); + return new HashMap<>(data); } public TbMsgMetaData copy() { - return new TbMsgMetaData(this.data); + return new TbMsgMetaData(data); } + + public boolean isEmpty() { + return data == null || data.isEmpty(); + } + } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java index 78ddba15d1..ae8faecb0b 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java @@ -16,11 +16,11 @@ package org.thingsboard.rule.engine.api.util; import com.fasterxml.jackson.databind.JsonNode; -import org.springframework.util.CollectionUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -29,15 +29,18 @@ import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; -/** - * Created by ashvayka on 19.01.18. - */ -public class TbNodeUtils { +public final class TbNodeUtils { + + private TbNodeUtils() { + throw new IllegalStateException("Utility class"); + } private static final Pattern DATA_PATTERN = Pattern.compile("(\\$\\[)(.*?)(])"); + private static final String ALL_DATA_TEMPLATE = "$[*]"; + private static final String ALL_METADATA_TEMPLATE = "${*}"; + public static T convert(TbNodeConfiguration configuration, Class clazz) throws TbNodeException { try { return JacksonUtil.treeToValue(configuration.getData(), clazz); @@ -47,16 +50,19 @@ public class TbNodeUtils { } public static List processPatterns(List patterns, TbMsg tbMsg) { - if (!CollectionUtils.isEmpty(patterns)) { - return patterns.stream().map(p -> processPattern(p, tbMsg)).collect(Collectors.toList()); + if (CollectionsUtil.isEmpty(patterns)) { + return Collections.emptyList(); } - return Collections.emptyList(); + return patterns.stream().map(p -> processPattern(p, tbMsg)).toList(); } public static String processPattern(String pattern, TbMsg tbMsg) { try { String result = processPattern(pattern, tbMsg.getMetaData()); JsonNode json = JacksonUtil.toJsonNode(tbMsg.getData()); + + result = result.replace(ALL_DATA_TEMPLATE, JacksonUtil.toString(json)); + if (json.isObject()) { Matcher matcher = DATA_PATTERN.matcher(result); while (matcher.find()) { @@ -64,7 +70,7 @@ public class TbNodeUtils { String[] keys = group.split("\\."); JsonNode jsonNode = json; for (String key : keys) { - if (!StringUtils.isEmpty(key) && jsonNode != null) { + if (StringUtils.isNotEmpty(key) && jsonNode != null) { jsonNode = jsonNode.get(key); } else { jsonNode = null; @@ -83,15 +89,9 @@ public class TbNodeUtils { } } - @Deprecated(since = "3.6.1", forRemoval = true) - public static List processPatterns(List patterns, TbMsgMetaData metaData) { - if (!CollectionUtils.isEmpty(patterns)) { - return patterns.stream().map(p -> processPattern(p, metaData)).collect(Collectors.toList()); - } - return Collections.emptyList(); - } - - public static String processPattern(String pattern, TbMsgMetaData metaData) { + private static String processPattern(String pattern, TbMsgMetaData metaData) { + String replacement = metaData.isEmpty() ? "{}" : JacksonUtil.toString(metaData.getData()); + pattern = pattern.replace(ALL_METADATA_TEMPLATE, replacement); return processTemplate(pattern, metaData.values()); } @@ -108,10 +108,11 @@ public class TbNodeUtils { } static String formatDataVarTemplate(String key) { - return "$[" + key + ']'; + return "$[" + key + "]"; } static String formatMetadataVarTemplate(String key) { - return "${" + key + '}'; + return "${" + key + "}"; } + } diff --git a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java index 7651a46b62..89d775f305 100644 --- a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java +++ b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java @@ -26,6 +26,8 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import java.util.Map; + import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -167,4 +169,160 @@ public class TbNodeUtilsTest { assertThat(TbNodeUtils.formatMetadataVarTemplate(null), is("${null}")); assertThat(TbNodeUtils.formatMetadataVarTemplate(null), is(String.format(METADATA_VARIABLE_TEMPLATE, (String) null))); } + + @Test + public void testAllMetadataTemplateReplacement() { + // GIVEN + String pattern = "META ${*}"; + var metadata = new TbMsgMetaData(); + metadata.putValue("meta_key", "meta_value"); + + var msg = TbMsg.newMsg() + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(metadata) + .build(); + + // WHEN + String actual = TbNodeUtils.processPattern(pattern, msg); + + // THEN + String expected = "META {\"meta_key\":\"meta_value\"}"; + assertThat(actual, is(expected)); + } + + @Test + public void testMultipleAllMetadataTemplatesReplacement() { + // GIVEN + String pattern = "${*} then again ${*}"; + var metadata = new TbMsgMetaData(); + metadata.putValue("meta_key", "meta_value"); + + var msg = TbMsg.newMsg() + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(metadata) + .build(); + + // WHEN + String actual = TbNodeUtils.processPattern(pattern, msg); + + // THEN + String expected = "{\"meta_key\":\"meta_value\"} then again {\"meta_key\":\"meta_value\"}"; + assertThat(actual, is(expected)); + } + + @Test + public void testAllDataTemplateReplacement() { + // GIVEN + String pattern = "DATA $[*]"; + var dataJson = JacksonUtil.newObjectNode().put("data_key", "data_value"); + + var msg = TbMsg.newMsg() + .data(JacksonUtil.toString(dataJson)) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + String actual = TbNodeUtils.processPattern(pattern, msg); + + // THEN + String expected = "DATA {\"data_key\":\"data_value\"}"; + assertThat(actual, is(expected)); + } + + @Test + public void testMultipleAllDataTemplatesReplacement() { + // GIVEN + String pattern = "$[*] then again $[*]"; + var dataJson = JacksonUtil.newObjectNode().put("data_key", "data_value"); + + var msg = TbMsg.newMsg() + .data(JacksonUtil.toString(dataJson)) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + String actual = TbNodeUtils.processPattern(pattern, msg); + + // THEN + String expected = "{\"data_key\":\"data_value\"} then again {\"data_key\":\"data_value\"}"; + assertThat(actual, is(expected)); + } + + @Test + public void testAllDataAndAllMetadataTemplatesSimultaneously() { + // GIVEN + String pattern = "META ${*} DATA $[*]"; + + var metadata = new TbMsgMetaData(Map.of("meta_key", "meta_value")); + var dataJson = JacksonUtil.newObjectNode().put("data_key", "data_value"); + + var msg = TbMsg.newMsg() + .data(JacksonUtil.toString(dataJson)) + .metaData(metadata) + .build(); + + // WHEN + String actual = TbNodeUtils.processPattern(pattern, msg); + + // THEN + String expected = "META {\"meta_key\":\"meta_value\"} DATA {\"data_key\":\"data_value\"}"; + assertThat(actual, is(expected)); + } + + @Test + public void testAllDataAndAllMetadataTemplatesSimultaneouslyEmpty() { + // GIVEN + String pattern = "META ${*} DATA $[*]"; + + var msg = TbMsg.newMsg() + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + String actual = TbNodeUtils.processPattern(pattern, msg); + + // THEN + String expected = "META {} DATA {}"; + assertThat(actual, is(expected)); + } + + @Test + public void testAllDataTemplateArray() { + // GIVEN + String pattern = "DATA $[*]"; + + var msg = TbMsg.newMsg() + .data("[1, \"two\", true]") + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + String actual = TbNodeUtils.processPattern(pattern, msg); + + // THEN + String expected = "DATA [1,\"two\",true]"; + assertThat(actual, is(expected)); + } + + @Test + public void testMixedAllDataMetadataAndNormalTemplates() { + // GIVEN + String pattern = "fullMeta=${*}, singleMeta=${meta_key}, fullData=$[*], singleData=$[data_key]"; + var metadata = new TbMsgMetaData(Map.of("meta_key", "meta_value")); + var dataJson = JacksonUtil.newObjectNode().put("data_key", "data_value"); + + var msg = TbMsg.newMsg() + .data(JacksonUtil.toString(dataJson)) + .metaData(metadata) + .build(); + + // WHEN + String actual = TbNodeUtils.processPattern(pattern, msg); + + // THEN + String expected = "fullMeta={\"meta_key\":\"meta_value\"}, singleMeta=meta_value, fullData={\"data_key\":\"data_value\"}, singleData=data_value"; + assertThat(actual, is(expected)); + } + } From c09639701084382ffae044402bcc8d4282ce1184 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 17 Jun 2025 18:39:16 +0300 Subject: [PATCH 060/435] AI rule node: support node patterns in prompts --- .../thingsboard/rule/engine/ai/TbAiNode.java | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 955442e79b..4d56b43c8e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -27,7 +27,6 @@ import dev.langchain4j.model.chat.request.ResponseFormat; import dev.langchain4j.model.chat.request.ResponseFormatType; import dev.langchain4j.model.chat.request.json.JsonSchema; import dev.langchain4j.model.chat.response.ChatResponse; -import dev.langchain4j.model.input.PromptTemplate; import org.checkerframework.checker.nullness.qual.NonNull; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; @@ -46,11 +45,9 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.dao.exception.DataValidationException; import java.util.List; -import java.util.Map; import java.util.NoSuchElementException; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; -import static java.util.Objects.requireNonNullElse; import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields; @RuleNode( @@ -63,8 +60,8 @@ import static org.thingsboard.server.dao.service.ConstraintValidator.validateFie ) public final class TbAiNode extends TbAbstractExternalNode implements TbNode { - private SystemMessage systemMessage; - private PromptTemplate userPromptTemplate; + private String systemPrompt; + private String userPrompt; private ResponseFormat responseFormat; private int timeoutSeconds; private AiSettingsId aiSettingsId; @@ -86,15 +83,8 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { .jsonSchema(getJsonSchema(config.getResponseFormatType(), config.getJsonSchema())) .build(); - systemMessage = SystemMessage.from(config.getSystemPrompt()); - userPromptTemplate = PromptTemplate.from(""" - User-provided task or question: %s - Rule engine message payload: {{msgPayload}} - Rule engine message metadata: {{msgMetadata}} - Rule engine message type: {{msgType}}""" - .formatted(config.getUserPrompt()) - ); - + systemPrompt = config.getSystemPrompt(); + userPrompt = config.getUserPrompt(); timeoutSeconds = config.getTimeoutSeconds(); if (!aiSettingsExist(ctx, config.getAiSettingsId())) { @@ -118,12 +108,8 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { var ackedMsg = ackIfNeeded(ctx, msg); - Map variables = Map.of( - "msgPayload", msg.getData(), - "msgMetadata", requireNonNullElse(JacksonUtil.toString(msg.getMetaData().getData()), "{}"), - "msgType", msg.getType() - ); - UserMessage userMessage = userPromptTemplate.apply(variables).toUserMessage(); + var systemMessage = SystemMessage.from(TbNodeUtils.processPattern(systemPrompt, ackedMsg)); + var userMessage = UserMessage.from(TbNodeUtils.processPattern(userPrompt, ackedMsg)); var chatRequest = ChatRequest.builder() .messages(List.of(systemMessage, userMessage)) @@ -183,8 +169,8 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { @Override public void destroy() { super.destroy(); - systemMessage = null; - userPromptTemplate = null; + systemPrompt = null; + userPrompt = null; responseFormat = null; aiSettingsId = null; } From 9bffff2d75dd5263a1ecc7fe4272fb841a299a76 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 17 Jun 2025 18:39:33 +0300 Subject: [PATCH 061/435] AI rule node: fix typo --- .../rule/engine/ai/Langchain4jJsonSchemaAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java index 12988c209d..e136bfaea8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java @@ -32,7 +32,7 @@ import java.util.ArrayList; import java.util.List; /** - * Converts a Jackson {@link JsonNode} JSON Schema into a Langchain4j {@link JsonSchema} model. + * Converts a Jackson {@link ObjectNode} JSON Schema into a Langchain4j {@link JsonSchema} model. */ final class Langchain4jJsonSchemaAdapter { From a843c86227b1b0daec226cbce7bc0d2f3998aa6d Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 18 Jun 2025 16:42:45 +0300 Subject: [PATCH 062/435] EDQS: human readable response on failed to send kafka msg --- .../server/queue/TbQueueHandler.java | 7 +++-- .../server/edqs/processor/EdqsProcessor.java | 19 +++++++++++++ .../PartitionedQueueResponseTemplate.java | 28 +++++++++++++++++-- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueHandler.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueHandler.java index bb5bc1d668..523a97e13c 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueHandler.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueHandler.java @@ -17,11 +17,12 @@ package org.thingsboard.server.queue; import com.google.common.util.concurrent.ListenableFuture; -/** - * Created by ashvayka on 05.10.18. - */ public interface TbQueueHandler { ListenableFuture handle(Request request); + default Response constructErrorResponseMsg(Request request, Throwable cause) { + return null; + } + } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index 0e74cb98fa..13a54973e6 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -203,6 +203,25 @@ public class EdqsProcessor implements TbQueueHandler, }); } + @Override + public TbProtoQueueMsg constructErrorResponseMsg(TbProtoQueueMsg request, Throwable e) { + EdqsResponse response = new EdqsResponse(); + String errorMessage; + if (e instanceof org.apache.kafka.common.errors.RecordTooLargeException) { + errorMessage = "Result set is too large"; + } else if (e instanceof IllegalArgumentException || e instanceof NullPointerException) { + errorMessage = "Invalid request format or missing data: " + ExceptionUtil.getMessage(e); + } else { + errorMessage = ExceptionUtil.getMessage(e); + } + response.setError(errorMessage); + return new TbProtoQueueMsg<>(request.getKey(), FromEdqsMsg.newBuilder() + .setResponseMsg(TransportProtos.EdqsResponseMsg.newBuilder() + .setValue(JacksonUtil.toString(response)) + .build()) + .build(), request.getHeaders()); + } + private EdqsResponse processRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request) { EdqsResponse response = new EdqsResponse(); try { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java index 7e913009f0..a15fafa19a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java @@ -21,9 +21,11 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.stats.MessagesStats; +import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueHandler; import org.thingsboard.server.queue.TbQueueMsg; +import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; @@ -119,8 +121,20 @@ public class PartitionedQueueResponseTemplate { pendingRequestCount.decrementAndGet(); response.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId)); - responseProducer.send(TopicPartitionInfo.builder().topic(responseTopic).build(), response, null); - stats.incrementSuccessful(); + TopicPartitionInfo tpi = TopicPartitionInfo.builder().topic(responseTopic).build(); + responseProducer.send(tpi, response, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + stats.incrementSuccessful(); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}] Failed to send response {}", requestId, response, t); + sendErrorResponse(requestId, responseTopic, request, t); + stats.incrementFailed(); + } + }); }, e -> { pendingRequestCount.decrementAndGet(); @@ -144,6 +158,16 @@ public class PartitionedQueueResponseTemplate partitions) { requestConsumer.update(partitions); } From 060f728fcd90b639ac7b9efb2f358e0ce2f857c8 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Wed, 18 Jun 2025 19:47:18 +0300 Subject: [PATCH 063/435] CalculatedField functionality support for Edge - fixed pushEventToAllRelatedEdges - refactoring --- .../calculated/BaseCalculatedFieldProcessor.java | 4 ++-- .../calculated/CalculatedFieldEdgeProcessor.java | 8 ++++---- .../thingsboard/server/edge/AbstractEdgeTest.java | 11 +++++++++++ .../server/edge/CalculatedFieldEdgeTest.java | 12 ++++-------- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java index da3051045f..a7781fe55d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java @@ -88,10 +88,10 @@ public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { return Pair.of(isCreated, isNameUpdated); } - protected ListenableFuture pushEventToAllRelatedEdges(TenantId tenantId, EntityId entityId, EdgeEventType type, EdgeEventActionType actionType, EdgeId sourceEdgeId) { + protected ListenableFuture pushEventToAllRelatedEdges(TenantId tenantId, EntityId calculatedFieldOwnerId, EntityId entityId, EdgeEventType type, EdgeEventActionType actionType, EdgeId sourceEdgeId) { List> futures = new ArrayList<>(); PageDataIterableByTenantIdEntityId edgeIds = - new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, entityId, RELATED_EDGES_CACHE_ITEMS); + new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, calculatedFieldOwnerId, RELATED_EDGES_CACHE_ITEMS); for (EdgeId relatedEdgeId : edgeIds) { if (!relatedEdgeId.equals(sourceEdgeId)) { futures.add(saveEdgeEvent(tenantId, relatedEdgeId, type, actionType, entityId, null)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java index cf28003707..a0212bb4df 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java @@ -126,15 +126,15 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i switch (actionType) { case UPDATED: case ADDED: - EntityId bodyEntityId = JacksonUtil.fromString(edgeNotificationMsg.getBody(), EntityId.class); - if (bodyEntityId != null && - (EntityType.DEVICE.equals(bodyEntityId.getEntityType()) || EntityType.ASSET.equals(bodyEntityId.getEntityType()))) { + EntityId calculatedFieldOwnerId = JacksonUtil.fromString(edgeNotificationMsg.getBody(), EntityId.class); + if (calculatedFieldOwnerId != null && + (EntityType.DEVICE.equals(calculatedFieldOwnerId.getEntityType()) || EntityType.ASSET.equals(calculatedFieldOwnerId.getEntityType()))) { JsonNode body = JacksonUtil.toJsonNode(edgeNotificationMsg.getBody()); EdgeId edgeId = safeGetEdgeId(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB()); return edgeId != null ? saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body) : - pushEventToAllRelatedEdges(tenantId, entityId, type, actionType, originatorEdgeId); + pushEventToAllRelatedEdges(tenantId, calculatedFieldOwnerId, entityId, type, actionType, originatorEdgeId); } else { return pushEventToAllEdges(tenantId, type, actionType, entityId, originatorEdgeId); } diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index 24fa02f11a..84d879c993 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -75,6 +75,7 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.edge.EdgeEventService; @@ -565,6 +566,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { protected Device saveDeviceOnCloudAndVerifyDeliveryToEdge() throws Exception { // create device and assign to edge Device savedDevice = saveDevice(StringUtils.randomAlphanumeric(15), thermostatDeviceProfile.getName()); + DeviceCredentials deviceCredentials = doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); edgeImitator.expectMessageAmount(3); // device and device profile messages and device credentials doPost("/api/edge/" + edge.getUuidId() + "/device/" + savedDevice.getUuidId(), Device.class); @@ -582,6 +584,15 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); Assert.assertEquals(thermostatDeviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB()); Assert.assertEquals(thermostatDeviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB()); + + Optional deviceCredentialsUpdateMsgOpt = edgeImitator.findMessageByType(DeviceCredentialsUpdateMsg.class); + Assert.assertTrue(deviceCredentialsUpdateMsgOpt.isPresent()); + DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = deviceCredentialsUpdateMsgOpt.get(); + DeviceCredentials deviceCredentialsMsg = JacksonUtil.fromString(deviceCredentialsUpdateMsg.getEntity(), DeviceCredentials.class, true); + Assert.assertNotNull(deviceCredentialsMsg); + Assert.assertEquals(savedDevice.getId(), deviceCredentialsMsg.getDeviceId()); + Assert.assertEquals(deviceCredentials, deviceCredentialsMsg); + return savedDevice; } diff --git a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java index da75225d11..5ec48daf63 100644 --- a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java @@ -58,16 +58,15 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); - edgeImitator.expectMessageAmount(SYNC_MESSAGE_COUNT + 4); + edgeImitator.expectMessageAmount(1); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); - doPost("/api/edge/sync/" + edge.getId()); Assert.assertTrue(edgeImitator.waitForMessages()); List downlinkMsgs = edgeImitator.getDownlinkMsgs(); AbstractMessage latestMessage = downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg instanceof CalculatedFieldUpdateMsg).findFirst().get(); Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; - Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); Assert.assertEquals(savedCalculatedField.getUuidId().getMostSignificantBits(), calculatedFieldUpdateMsg.getIdMSB()); Assert.assertEquals(savedCalculatedField.getUuidId().getLeastSignificantBits(), calculatedFieldUpdateMsg.getIdLSB()); CalculatedField calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); @@ -77,11 +76,9 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedDevice.getId(), calculatedFieldFromMsg.getEntityId()); Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration()); - // update calculatedField - edgeImitator.expectMessageAmount(SYNC_MESSAGE_COUNT + 4); + edgeImitator.expectMessageAmount(1); savedCalculatedField.setName(UPDATED_CF_NAME); savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); - doPost("/api/edge/sync/" + edge.getId()); Assert.assertTrue(edgeImitator.waitForMessages()); downlinkMsgs = edgeImitator.getDownlinkMsgs(); latestMessage = downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg instanceof CalculatedFieldUpdateMsg).findFirst().get(); @@ -144,9 +141,8 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); - edgeImitator.expectMessageAmount(SYNC_MESSAGE_COUNT + 4); + edgeImitator.expectMessageAmount(1); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); - doPost("/api/edge/sync/" + edge.getId()); Assert.assertTrue(edgeImitator.waitForMessages()); UUID uuid = Uuids.timeBased(); From 6f42d5fb77ee9d4cba91671c40d135f00d4ad274 Mon Sep 17 00:00:00 2001 From: yevhenii Date: Thu, 19 Jun 2025 11:17:16 +0300 Subject: [PATCH 064/435] CalculatedField functionality support for Edge - refactoring test --- .../thingsboard/server/edge/CalculatedFieldEdgeTest.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java index 5ec48daf63..0eabba10f9 100644 --- a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java @@ -38,7 +38,6 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -62,8 +61,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); Assert.assertTrue(edgeImitator.waitForMessages()); - List downlinkMsgs = edgeImitator.getDownlinkMsgs(); - AbstractMessage latestMessage = downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg instanceof CalculatedFieldUpdateMsg).findFirst().get(); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); @@ -80,8 +78,8 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { savedCalculatedField.setName(UPDATED_CF_NAME); savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); Assert.assertTrue(edgeImitator.waitForMessages()); - downlinkMsgs = edgeImitator.getDownlinkMsgs(); - latestMessage = downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg instanceof CalculatedFieldUpdateMsg).findFirst().get(); + + latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); @@ -94,6 +92,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { doDelete("/api/calculatedField/" + savedCalculatedField.getUuidId()) .andExpect(status().isOk()); Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; From b747b6b1bfb6df426b278f14e418ea11779679ae Mon Sep 17 00:00:00 2001 From: yevhenii Date: Thu, 19 Jun 2025 13:37:06 +0300 Subject: [PATCH 065/435] CalculatedField functionality support for Edge - added new edge version --- common/edge-api/src/main/proto/edge.proto | 1 + 1 file changed, 1 insertion(+) diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 904a938f1e..23eafc4574 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -42,6 +42,7 @@ enum EdgeVersion { V_3_8_0 = 8; V_3_9_0 = 9; V_4_0_0 = 10; + V_4_1_0 = 11; V_LATEST = 999; } From 7dc8f107408aa50c17bac9a244fe5ac8369a6c1c Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 19 Jun 2025 17:09:08 +0300 Subject: [PATCH 066/435] Refactoring --- .../impl/DeviceProfileImportService.java | 10 +++++---- .../server/common/data/id/OtaPackageId.java | 2 ++ .../create/AutoVersionCreateConfig.java | 3 +++ .../server/dao/ota/BaseOtaPackageService.java | 22 ++++++++----------- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java index c867286285..e498cf0911 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java @@ -52,12 +52,14 @@ public class DeviceProfileImportService extends BaseEntityImportService exportData, IdProvider idProvider, CompareResult compareResult) { + boolean toUpdate = ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds(); + if (toUpdate) { + deviceProfile.setFirmwareId(idProvider.getInternalId(deviceProfile.getFirmwareId())); + deviceProfile.setSoftwareId(idProvider.getInternalId(deviceProfile.getSoftwareId())); + } DeviceProfile saved = deviceProfileService.saveDeviceProfile(deviceProfile); - if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) { + if (toUpdate) { importCalculatedFields(ctx, saved, exportData, idProvider); - saved.setFirmwareId(idProvider.getInternalId(deviceProfile.getFirmwareId())); - saved.setSoftwareId(idProvider.getInternalId(deviceProfile.getSoftwareId())); - saved = deviceProfileService.saveDeviceProfile(saved); } return saved; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java index 2a8efc5bfb..a246ab5f94 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java @@ -20,10 +20,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; public class OtaPackageId extends UUIDBased implements EntityId { + @Serial private static final long serialVersionUID = 1L; @JsonCreator diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java index 8e61991b38..b5e2813134 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java @@ -18,10 +18,13 @@ package org.thingsboard.server.common.data.sync.vc.request.create; import lombok.Data; import lombok.EqualsAndHashCode; +import java.io.Serial; + @EqualsAndHashCode(callSuper = true) @Data public class AutoVersionCreateConfig extends VersionCreateConfig { + @Serial private static final long serialVersionUID = 8245450889383315551L; private String branch; diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 83cb35d152..7aff945e84 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -74,7 +74,7 @@ public class BaseOtaPackageService extends AbstractCachedEntityService Date: Thu, 19 Jun 2025 17:15:19 +0300 Subject: [PATCH 067/435] Minor improvement of saveOtaPackageInfo --- .../org/thingsboard/server/dao/ota/BaseOtaPackageService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 7aff945e84..343a2485ce 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -74,7 +74,7 @@ public class BaseOtaPackageService extends AbstractCachedEntityService Date: Fri, 20 Jun 2025 12:21:29 +0300 Subject: [PATCH 068/435] AI rule node: bump Langchain4j version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c68cc9e1e1..c9303e9715 100755 --- a/pom.xml +++ b/pom.xml @@ -134,7 +134,7 @@ 1.7.5 3.8.0 2.9.0 - 1.0.1 + 1.1.0 4.2.1 2.7.3 From b76909ce05cb2e8ce1069860c370e108ee3f1307 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 20 Jun 2025 15:15:15 +0300 Subject: [PATCH 069/435] Fix ota save when saving url --- .../sync/ie/importing/impl/OtaPackageImportService.java | 5 +++++ .../dao/service/validator/OtaPackageDataValidator.java | 1 + 2 files changed, 6 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java index 16b204411e..fd8a7822eb 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java @@ -19,6 +19,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackage; +import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.sync.ie.OtaPackageExportData; @@ -60,6 +61,10 @@ public class OtaPackageImportService extends BaseEntityImportService Date: Fri, 20 Jun 2025 15:54:01 +0300 Subject: [PATCH 070/435] Fix error on import --- .../sync/ie/importing/impl/OtaPackageImportService.java | 2 +- .../java/org/thingsboard/server/common/data/OtaPackage.java | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java index fd8a7822eb..9cd6bb50c6 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java @@ -63,7 +63,7 @@ public class OtaPackageImportService extends BaseEntityImportService Date: Wed, 18 Jun 2025 11:30:00 +0300 Subject: [PATCH 071/435] UI: Remove MQTT v5, MQTT v3.1 options for "Azure IoT Hub" rule node. --- .../rule-node/external/azure-iot-hub-config.component.html | 4 +++- .../rule-node/external/azure-iot-hub-config.component.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html index 79c4a2f00d..bab0c6b196 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html @@ -38,7 +38,9 @@ {{ 'rule-node-config.device-id-required' | translate }} - + + diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts index 7dd934ccd9..dff5f06e54 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts @@ -22,6 +22,7 @@ import { azureIotHubCredentialsTypes, azureIotHubCredentialsTypeTranslations } from '@home/components/rule-node/rule-node-config.models'; +import { MqttVersion } from '@shared/models/mqtt.models'; @Component({ selector: 'tb-external-node-azure-iot-hub-config', @@ -34,6 +35,7 @@ export class AzureIotHubConfigComponent extends RuleNodeConfigurationComponent { allAzureIotHubCredentialsTypes = azureIotHubCredentialsTypes; azureIotHubCredentialsTypeTranslationsMap = azureIotHubCredentialsTypeTranslations; + MqttVersion = MqttVersion; constructor(private fb: UntypedFormBuilder) { super(); From ef385c4fa910a18c91138fb942cb439adfc27121 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 6 Jun 2025 14:39:03 +0300 Subject: [PATCH 072/435] UI: Mqtt vesion add excludeVersion. --- .../shared/components/mqtt-version-select.component.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts b/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts index 6bde856a7b..5145a7fd51 100644 --- a/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts +++ b/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts @@ -41,7 +41,13 @@ export class MqttVersionSelectComponent implements ControlValueAccessor { @Input() appearance: MatFormFieldAppearance = 'fill'; - mqttVersions = Object.values(MqttVersion); + @Input() + excludeVersions: MqttVersion[]; + + get mqttVersions(): MqttVersion[] { + return Object.values(MqttVersion).filter(v => !this.excludeVersions || !this.excludeVersions.includes(v)); + } + mqttVersionTranslation = MqttVersionTranslation; modelValue: MqttVersion; From 050fa670af450c89ad6e84372004d558c6c85ffb Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 6 Jun 2025 17:37:51 +0300 Subject: [PATCH 073/435] UI: Minor improvement of excludeVersions in mqtt version select. --- .../mqtt-version-select.component.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts b/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts index 5145a7fd51..0a9b75be2d 100644 --- a/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts +++ b/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input } from '@angular/core'; +import { Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { coerceBoolean } from '@shared/decorators/coercion'; import { SubscriptSizing, MatFormFieldAppearance } from '@angular/material/form-field'; @@ -30,7 +30,7 @@ import { MqttVersionTranslation, MqttVersion } from '@shared/models/mqtt.models' multi: true }] }) -export class MqttVersionSelectComponent implements ControlValueAccessor { +export class MqttVersionSelectComponent implements ControlValueAccessor, OnChanges { @Input() disabled: boolean; @@ -44,10 +44,7 @@ export class MqttVersionSelectComponent implements ControlValueAccessor { @Input() excludeVersions: MqttVersion[]; - get mqttVersions(): MqttVersion[] { - return Object.values(MqttVersion).filter(v => !this.excludeVersions || !this.excludeVersions.includes(v)); - } - + mqttVersions = Object.values(MqttVersion); mqttVersionTranslation = MqttVersionTranslation; modelValue: MqttVersion; @@ -60,6 +57,20 @@ export class MqttVersionSelectComponent implements ControlValueAccessor { constructor() { } + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (propName === 'excludeVersions' && change.currentValue !== change.previousValue) { + const excludeVersions = change.currentValue; + if (excludeVersions?.length) { + this.mqttVersions = Object.values(MqttVersion).filter(v => !excludeVersions.includes(v)); + } else { + this.mqttVersions = Object.values(MqttVersion); + } + } + } + } + registerOnChange(fn: any): void { this.propagateChange = fn; } From 0be43296e1d94b680ca7ffcdd011da9535b2524a Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Fri, 20 Jun 2025 18:41:03 +0300 Subject: [PATCH 074/435] EdgeGrpcSession - improve logs on delivery failues --- .../thingsboard/server/service/edge/rpc/EdgeGrpcSession.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index b6ecd848ad..94e0c155fe 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -452,14 +452,15 @@ public abstract class EdgeGrpcSession implements Closeable { List copy = new ArrayList<>(sessionState.getPendingMsgsMap().values()); if (attempt > 1) { String error = "Failed to deliver the batch"; - String failureMsg = String.format("{%s}: {%s}", error, copy); + String failureMsg = String.format("{%s} (size: {%s})", error, copy.size()); if (attempt == 2) { // Send a failure notification only on the second attempt. // This ensures that failure alerts are sent just once to avoid redundant notifications. ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId) .edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); } - log.warn("[{}][{}] {}, attempt: {}", tenantId, edge.getId(), failureMsg, attempt); + log.warn("[{}][{}] {} on attempt {}", tenantId, edge.getId(), failureMsg, attempt); + log.debug("[{}][{}] entities in failed batch: {}", tenantId, edge.getId(), copy); } log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", tenantId, edge.getId(), copy.size()); for (DownlinkMsg downlinkMsg : copy) { From 7a81ec5a8b0aa5bc643ec7496bccb433473d3498 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 23 Jun 2025 11:45:41 +0300 Subject: [PATCH 075/435] Add name to OtaPackageInfo json --- .../org/thingsboard/server/common/data/OtaPackageInfo.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java index 2930ff41d1..9f010db823 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -124,7 +125,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp } @Override - @JsonIgnore + @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String getName() { return title; } From eb1e3dfabfe725f8fb80bccff1b4d10f9348d983 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 23 Jun 2025 12:28:46 +0300 Subject: [PATCH 076/435] UI: Version Control for OTA updates --- .../modules/home/pages/admin/admin.module.ts | 2 + .../resource-library-tabs.component.html | 23 ++++++++++ .../resource-library-tabs.component.ts | 36 +++++++++++++++ .../resources-library-table-config.resolve.ts | 2 + .../ota-update-table-config.resolve.ts | 2 + .../ota-update/ota-update-tabs.component.html | 23 ++++++++++ .../ota-update/ota-update-tabs.component.ts | 44 +++++++++++++++++++ .../pages/ota-update/ota-update.module.ts | 4 +- .../app/shared/models/entity-type.models.ts | 2 + .../app/shared/models/ota-package.models.ts | 4 +- ui-ngx/src/app/shared/models/vc.models.ts | 6 ++- .../assets/locale/locale.constant-en_US.json | 2 + 12 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index a5f18122fd..10721ade5a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -38,6 +38,7 @@ import { JsLibraryTableHeaderComponent } from '@home/pages/admin/resource/js-lib import { JsResourceComponent } from '@home/pages/admin/resource/js-resource.component'; import { NgxFlowModule } from '@flowjs/ngx-flow'; import { TrendzSettingsComponent } from '@home/pages/admin/trendz-settings.component'; +import { ResourceLibraryTabsComponent } from '@home/pages/admin/resource/resource-library-tabs.component'; @NgModule({ declarations: @@ -50,6 +51,7 @@ import { TrendzSettingsComponent } from '@home/pages/admin/trendz-settings.compo HomeSettingsComponent, ResourcesLibraryComponent, ResourceTabsComponent, + ResourceLibraryTabsComponent, ResourcesTableHeaderComponent, JsResourceComponent, JsLibraryTableHeaderComponent, diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html new file mode 100644 index 0000000000..4effdaad53 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html @@ -0,0 +1,23 @@ + + + + diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts new file mode 100644 index 0000000000..a85bf48db3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts @@ -0,0 +1,36 @@ +/// +/// 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. +/// + +import { Component } from '@angular/core'; +import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component'; +import { Resource } from '@shared/models/resource.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; + +@Component({ + selector: 'tb-resource-library-tabs', + templateUrl: './resource-library-tabs.component.html', + styleUrls: [] +}) +export class ResourceLibraryTabsComponent extends EntityTabsComponent { + + readonly NULL_UUID = NULL_UUID; + + constructor(protected store: Store) { + super(store); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts index 91327b7465..f39ed9ff7b 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts @@ -37,6 +37,7 @@ import { PageLink } from '@shared/models/page/page-link'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { map } from 'rxjs/operators'; import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component'; +import { ResourceLibraryTabsComponent } from '@home/pages/admin/resource/resource-library-tabs.component'; @Injectable() export class ResourcesLibraryTableConfigResolver { @@ -55,6 +56,7 @@ export class ResourcesLibraryTableConfigResolver { this.config.entityTranslations = entityTypeTranslations.get(EntityType.TB_RESOURCE); this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE); this.config.headerComponent = ResourcesTableHeaderComponent; + this.config.entityTabsComponent = ResourceLibraryTabsComponent; this.config.entityTitle = (resource) => resource ? resource.title : ''; diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts index cc3ca4046b..0dae778d03 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts @@ -36,6 +36,7 @@ import { PageLink } from '@shared/models/page/page-link'; import { OtaUpdateComponent } from '@home/pages/ota-update/ota-update.component'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { FileSizePipe } from '@shared/pipe/file-size.pipe'; +import { OtaUpdateTabsComponent } from '@home/pages/ota-update/ota-update-tabs.component'; @Injectable() export class OtaUpdateTableConfigResolve { @@ -50,6 +51,7 @@ export class OtaUpdateTableConfigResolve { private fileSize: FileSizePipe) { this.config.entityType = EntityType.OTA_PACKAGE; this.config.entityComponent = OtaUpdateComponent; + this.config.entityTabsComponent = OtaUpdateTabsComponent; this.config.entityTranslations = entityTypeTranslations.get(EntityType.OTA_PACKAGE); this.config.entityResources = entityTypeResources.get(EntityType.OTA_PACKAGE); diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html new file mode 100644 index 0000000000..a8cdae4256 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html @@ -0,0 +1,23 @@ + + + + diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts new file mode 100644 index 0000000000..1de3031ccb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts @@ -0,0 +1,44 @@ +/// +/// 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. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { OtaPackage } from '@shared/models/ota-package.models'; + +@Component({ + selector: 'tb-ota-update-tabs', + templateUrl: './ota-update-tabs.component.html', + styleUrls: [] +}) +export class OtaUpdateTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + isTenantOtaUpdate() { + return this.entity && this.entity.tenantId.id !== NULL_UUID; + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts index 139591f14f..fe24da31b2 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts @@ -20,10 +20,12 @@ import { SharedModule } from '@shared/shared.module'; import { HomeComponentsModule } from '@home/components/home-components.module'; import { OtaUpdateRoutingModule } from '@home/pages/ota-update/ota-update-routing.module'; import { OtaUpdateComponent } from '@home/pages/ota-update/ota-update.component'; +import { OtaUpdateTabsComponent } from '@home/pages/ota-update/ota-update-tabs.component'; @NgModule({ declarations: [ - OtaUpdateComponent + OtaUpdateComponent, + OtaUpdateTabsComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 48cad42e7b..60e2956e7c 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -346,6 +346,8 @@ export const entityTypeTranslations = new Map, HasTenantId { +export interface OtaPackageInfo extends BaseData, HasTenantId, ExportableEntity { tenantId?: TenantId; type: OtaUpdateType; deviceProfileId?: DeviceProfileId; diff --git a/ui-ngx/src/app/shared/models/vc.models.ts b/ui-ngx/src/app/shared/models/vc.models.ts index 3795518ffc..ebd7840f61 100644 --- a/ui-ngx/src/app/shared/models/vc.models.ts +++ b/ui-ngx/src/app/shared/models/vc.models.ts @@ -33,16 +33,18 @@ export const exportableEntityTypes: Array = [ EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE, EntityType.TB_RESOURCE, + EntityType.OTA_PACKAGE, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE ]; -export const entityTypesWithoutRelatedData: Set = new Set([ +export const entityTypesWithoutRelatedData = new Set([ EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, - EntityType.TB_RESOURCE + EntityType.TB_RESOURCE, + EntityType.OTA_PACKAGE, ]); export interface VersionCreateConfig { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 55e136e031..a63089bb6c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2584,6 +2584,8 @@ "type-tb-resources": "Resources", "list-of-tb-resources": "{ count, plural, =1 {One resource} other {List of # resources} }", "type-ota-package": "OTA package", + "type-ota-packages": "OTA packages", + "list-of-ota-packages": "{ count, plural, =1 {One OTA package} other {List of # OTA packages} }", "type-rpc": "RPC", "type-queue": "Queue", "type-queue-stats": "Queue statistics", From 6014eed8525fb6469c71f96fa5aa4945807aad4a Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Mon, 23 Jun 2025 16:10:03 +0300 Subject: [PATCH 077/435] Improved Kafka Edge Session destroy logic - added retry attempts to avoid unclosed consumers --- .../service/edge/rpc/EdgeGrpcService.java | 21 ++++++++++++++----- .../service/edge/rpc/EdgeGrpcSession.java | 4 +++- .../edge/rpc/KafkaEdgeGrpcSession.java | 14 +++++++++---- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index 5671ffb2ab..eaef1f7c7d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -94,6 +94,8 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS @TbCoreComponent public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService { + private static final int DESTROY_SESSION_MAX_ATTEMPTS = 10; + private final ConcurrentMap sessions = new ConcurrentHashMap<>(); private final ConcurrentMap sessionNewEventsLocks = new ConcurrentHashMap<>(); private final Map sessionNewEvents = new HashMap<>(); @@ -283,9 +285,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i EdgeGrpcSession session = sessions.get(edgeId); if (session != null && session.isConnected()) { log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId); - session.destroy(); + destroySession(session); session.cleanUp(); - session.close(); sessions.remove(edgeId); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); newEventLock.lock(); @@ -521,7 +522,15 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private void destroySession(EdgeGrpcSession session) { try (session) { - session.destroy(); + for (int i = 0; i < DESTROY_SESSION_MAX_ATTEMPTS; i++) { + if (session.destroy()) { + break; + } else { + try { + Thread.sleep(100); + } catch (InterruptedException ignored) {} + } + } } } @@ -643,9 +652,11 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } for (EdgeId edgeId : toRemove) { log.info("[{}] Destroying session for edge because edge is not connected", edgeId); - EdgeGrpcSession removed = sessions.remove(edgeId); + EdgeGrpcSession removed = sessions.get(edgeId); if (removed instanceof KafkaEdgeGrpcSession kafkaSession) { - kafkaSession.destroy(); + if (kafkaSession.destroy()) { + sessions.remove(edgeId); + } } } } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 94e0c155fe..c658bcf403 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -918,7 +918,9 @@ public abstract class EdgeGrpcSession implements Closeable { return Futures.allAsList(result); } - protected void destroy() {} + protected boolean destroy() { + return true; + } protected void cleanUp() {} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java index daffe9db11..ab0b42abb4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java @@ -135,19 +135,25 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession { } @Override - public void destroy() { + public boolean destroy() { try { if (consumer != null) { consumer.stop(); } - } finally { - consumer = null; + } catch (Exception e) { + log.warn("[{}][{}] Failed to stop edge event consumer", tenantId, edge.getId(), e); + return false; } + consumer = null; try { if (consumerExecutor != null) { consumerExecutor.shutdown(); } - } catch (Exception ignored) {} + } catch (Exception e) { + log.warn("[{}][{}] Failed to shutdown consumer executor", tenantId, edge.getId(), e); + return false; + } + return true; } @Override From 1343c4af3bc108e0a68b32cc0171ea0559d88202 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 23 Jun 2025 16:16:42 +0300 Subject: [PATCH 078/435] AI rule node: refactor model config data structure; rename from AI settings to AI model settings --- .../main/data/upgrade/basic/schema_update.sql | 9 +- .../server/actors/ActorSystemContext.java | 8 +- .../actors/ruleChain/DefaultTbContext.java | 12 +- ...er.java => AiModelSettingsController.java} | 74 ++++++------ .../server/controller/BaseController.java | 20 ++-- .../server/service/ai/AiModelServiceImpl.java | 37 ++++++ .../server/service/ai/AiServiceImpl.java | 106 ----------------- .../Langchain4jChatModelConfigurerImpl.java | 70 +++++++++++ .../edge/EdgeEventSourcingListener.java | 4 +- .../edge/RelatedEdgesSourcingListener.java | 2 +- .../entitiy/EntityStateSourcingListener.java | 4 +- ...a => DefaultTbAiModelSettingsService.java} | 30 ++--- ...ice.java => TbAiModelSettingsService.java} | 8 +- .../queue/DefaultTbClusterService.java | 2 +- .../service/security/permission/Resource.java | 2 +- .../permission/TenantAdminPermissions.java | 10 +- .../src/main/resources/thingsboard.yml | 8 +- ...rvice.java => AiModelSettingsService.java} | 18 +-- common/data/pom.xml | 4 + .../server/common/data/CacheConstants.java | 2 +- .../server/common/data/EntityType.java | 4 +- .../{AiSettings.java => AiModelSettings.java} | 50 ++------ .../server/common/data/ai/model/AiModel.java | 48 ++++++++ .../common/data/ai/model/AiModelConfig.java | 40 +------ .../common/data/ai/model/AiModelType.java | 22 ++++ .../model/GoogleAiGeminiChatModelConfig.java | 62 ---------- .../ai/model/MistralAiChatModelConfig.java | 62 ---------- .../data/ai/model/OpenAiChatModelConfig.java | 62 ---------- .../data/ai/model/chat/AiChatModel.java | 38 ++++++ .../data/ai/model/chat/AiChatModelConfig.java | 35 ++++++ .../model/chat/GoogleAiGeminiChatModel.java | 60 ++++++++++ .../chat/Langchain4jChatModelConfigurer.java | 28 +++++ .../ai/model/chat/MistralAiChatModel.java | 60 ++++++++++ .../data/ai/model/chat/OpenAiChatModel.java | 60 ++++++++++ .../data/ai/provider/AiProviderConfig.java | 22 +--- .../GoogleAiGeminiProviderConfig.java | 28 ++--- .../ai/provider/MistralAiProviderConfig.java | 28 ++--- .../ai/provider/OpenAiProviderConfig.java | 28 ++--- ...SettingsId.java => AiModelSettingsId.java} | 16 +-- .../common/data/id/EntityIdFactory.java | 2 +- common/proto/src/main/proto/queue.proto | 2 +- ...va => AiModelSettingsCacheEvictEvent.java} | 8 +- ...eKey.java => AiModelSettingsCacheKey.java} | 14 +-- ...java => AiModelSettingsCaffeineCache.java} | 10 +- ...ttingsDao.java => AiModelSettingsDao.java} | 12 +- ...he.java => AiModelSettingsRedisCache.java} | 8 +- ...l.java => AiModelSettingsServiceImpl.java} | 84 +++++++------- .../dao/housekeeper/CleanUpService.java | 2 +- .../server/dao/model/ModelConstants.java | 15 +-- ...Entity.java => AiModelSettingsEntity.java} | 64 ++++------ .../AiModelSettingsDataValidator.java | 86 ++++++++++++++ .../validator/AiSettingsDataValidator.java | 109 ------------------ ...ry.java => AiModelSettingsRepository.java} | 28 ++--- .../server/dao/sql/ai/JpaAiSettingsDao.java | 44 +++---- .../server/dao/tenant/TenantServiceImpl.java | 2 +- .../main/resources/sql/schema-entities.sql | 9 +- ...ice.java => RuleEngineAiModelService.java} | 12 +- .../rule/engine/api/TbContext.java | 6 +- .../thingsboard/rule/engine/ai/TbAiNode.java | 54 +++++---- .../rule/engine/ai/TbAiNodeConfiguration.java | 4 +- .../rule/engine/util/TenantIdLoader.java | 6 +- .../rule/engine/util/TenantIdLoaderTest.java | 16 +-- 62 files changed, 901 insertions(+), 879 deletions(-) rename application/src/main/java/org/thingsboard/server/controller/{AiSettingsController.java => AiModelSettingsController.java} (65%) create mode 100644 application/src/main/java/org/thingsboard/server/service/ai/AiModelServiceImpl.java delete mode 100644 application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java create mode 100644 application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java rename application/src/main/java/org/thingsboard/server/service/entitiy/ai/{DefaultTbAiSettingsService.java => DefaultTbAiModelSettingsService.java} (60%) rename application/src/main/java/org/thingsboard/server/service/entitiy/ai/{TbAiSettingsService.java => TbAiModelSettingsService.java} (76%) rename common/dao-api/src/main/java/org/thingsboard/server/dao/ai/{AiSettingsService.java => AiModelSettingsService.java} (55%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/{AiSettings.java => AiModelSettings.java} (52%) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelType.java delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/GoogleAiGeminiChatModelConfig.java delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/MistralAiChatModelConfig.java delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/OpenAiChatModelConfig.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java rename common/data/src/main/java/org/thingsboard/server/common/data/id/{AiSettingsId.java => AiModelSettingsId.java} (72%) rename dao/src/main/java/org/thingsboard/server/dao/ai/{AiSettingsCacheEvictEvent.java => AiModelSettingsCacheEvictEvent.java} (68%) rename dao/src/main/java/org/thingsboard/server/dao/ai/{AiSettingsCacheKey.java => AiModelSettingsCacheKey.java} (70%) rename dao/src/main/java/org/thingsboard/server/dao/ai/{AiSettingsCaffeineCache.java => AiModelSettingsCaffeineCache.java} (75%) rename dao/src/main/java/org/thingsboard/server/dao/ai/{AiSettingsDao.java => AiModelSettingsDao.java} (64%) rename dao/src/main/java/org/thingsboard/server/dao/ai/{AiSettingsRedisCache.java => AiModelSettingsRedisCache.java} (73%) rename dao/src/main/java/org/thingsboard/server/dao/ai/{AiSettingsServiceImpl.java => AiModelSettingsServiceImpl.java} (50%) rename dao/src/main/java/org/thingsboard/server/dao/model/sql/{AiSettingsEntity.java => AiModelSettingsEntity.java} (53%) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/service/validator/AiSettingsDataValidator.java rename dao/src/main/java/org/thingsboard/server/dao/sql/ai/{AiSettingsRepository.java => AiModelSettingsRepository.java} (57%) rename rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/{RuleEngineAiService.java => RuleEngineAiModelService.java} (60%) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 9685847278..f77c8dce67 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -14,15 +14,12 @@ -- limitations under the License. -- -CREATE TABLE ai_settings ( +CREATE TABLE ai_model_settings ( id UUID NOT NULL PRIMARY KEY, created_time BIGINT NOT NULL, tenant_id UUID NOT NULL, version BIGINT NOT NULL DEFAULT 1, name VARCHAR(255) NOT NULL, - provider VARCHAR(255) NOT NULL, - provider_config JSONB NOT NULL, - model VARCHAR(255) NOT NULL, - model_config JSONB, - CONSTRAINT ai_settings_name_unq_key UNIQUE (tenant_id, name) + configuration JSONB NOT NULL, + CONSTRAINT ai_model_settings_name_unq_key UNIQUE (tenant_id, name) ); diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 5fc7247061..a81e312bf6 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -35,7 +35,7 @@ import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.NotificationCenter; -import org.thingsboard.rule.engine.api.RuleEngineAiService; +import org.thingsboard.rule.engine.api.RuleEngineAiModelService; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; @@ -63,7 +63,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.stats.TbApiUsageReportClient; -import org.thingsboard.server.dao.ai.AiSettingsService; +import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -314,11 +314,11 @@ public class ActorSystemContext { @Autowired @Getter - private RuleEngineAiService aiService; + private RuleEngineAiModelService aiModelService; @Autowired @Getter - private AiSettingsService aiSettingsService; + private AiModelSettingsService aiModelSettingsService; @Autowired @Getter diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 9df08b7b87..5778aba4ef 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -28,7 +28,7 @@ import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.NotificationCenter; -import org.thingsboard.rule.engine.api.RuleEngineAiService; +import org.thingsboard.rule.engine.api.RuleEngineAiModelService; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; @@ -77,7 +77,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgProcessingStackItem; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.dao.ai.AiSettingsService; +import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -1016,13 +1016,13 @@ public class DefaultTbContext implements TbContext { } @Override - public RuleEngineAiService getAiService() { - return mainCtx.getAiService(); + public RuleEngineAiModelService getAiModelService() { + return mainCtx.getAiModelService(); } @Override - public AiSettingsService getAiSettingsService() { - return mainCtx.getAiSettingsService(); + public AiModelSettingsService getAiModelSettingsService() { + return mainCtx.getAiModelSettingsService(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java similarity index 65% rename from application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java rename to application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java index 530fc1e56a..891f6b176c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java @@ -26,9 +26,9 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.service.security.permission.Operation; @@ -47,70 +47,70 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERT import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; @RestController -@RequestMapping("/api/ai-settings") -public class AiSettingsController extends BaseController { +@RequestMapping("/api/ai-model-settings") +public class AiModelSettingsController extends BaseController { - private static final Set ALLOWED_SORT_PROPERTIES = Set.of("createdTime", "name", "provider", "model"); + private static final Set ALLOWED_SORT_PROPERTIES = Set.of("createdTime", "name"); @ApiOperation( - value = "Create or update AI settings (saveAiSettings)", - notes = "Creates or updates an AI settings record.\n\n" + + value = "Create or update AI model settings (saveAiModelSettings)", + notes = "Creates or updates an AI model settings record.\n\n" + "• **Create:** Omit the `id` to create a new record. The platform assigns a UUID to the new settings and returns it in the `id` field of the response.\n\n" + "• **Update:** Include an existing `id` to modify that record. If no matching record exists, the API responds with **404 Not Found**.\n\n" + - "Tenant ID for the AI settings will be taken from the authenticated user making the request, regardless of any value provided in the request body." + + "Tenant ID for the AI model settings will be taken from the authenticated user making the request, regardless of any value provided in the request body." + TENANT_AUTHORITY_PARAGRAPH ) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping - public AiSettings saveAiSettings(@RequestBody AiSettings aiSettings) throws ThingsboardException { + public AiModelSettings saveAiModelSettings(@RequestBody AiModelSettings settings) throws ThingsboardException { var user = getCurrentUser(); - aiSettings.setTenantId(user.getTenantId()); - checkEntity(aiSettings.getId(), aiSettings, Resource.AI_SETTINGS); - return tbAiSettingsService.save(aiSettings, user); + settings.setTenantId(user.getTenantId()); + checkEntity(settings.getId(), settings, Resource.AI_MODEL_SETTINGS); + return tbAiModelSettingsService.save(settings, user); } @ApiOperation( - value = "Get AI settings by ID (getAiSettingsById)", - notes = "Fetches an AI settings record by its `id`." + + value = "Get AI model settings by ID (getAiModelSettingsById)", + notes = "Fetches an AI model settings record by its `id`." + TENANT_AUTHORITY_PARAGRAPH ) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping("/{aiSettingsId}") - public AiSettings getAiSettingsById( + @GetMapping("/{aiModelSettingsId}") + public AiModelSettings getAiModelSettingsById( @Parameter( - description = "ID of the AI settings record", + description = "ID of the AI model settings record", required = true, example = "de7900d4-30e2-11f0-9cd2-0242ac120002" ) - @PathVariable("aiSettingsId") UUID aiSettingsUuid + @PathVariable("aiModelSettingsId") UUID aiModelSettingsUuid ) throws ThingsboardException { - return checkAiSettingsId(new AiSettingsId(aiSettingsUuid), Operation.READ); + return checkAiModelSettingsId(new AiModelSettingsId(aiModelSettingsUuid), Operation.READ); } @ApiOperation( - value = "Get AI settings (getAiSettings)", - notes = "Returns a page of AI settings. " + + value = "Get AI model settings (getAiModelSettings)", + notes = "Returns a page of AI model settings. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH ) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping - public PageData getAiSettings( + public PageData getAiModelSettings( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, @Parameter(description = AI_SETTINGS_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "provider", "model"})) + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty, @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder ) throws ThingsboardException { var user = getCurrentUser(); - accessControlService.checkPermission(user, Resource.AI_SETTINGS, Operation.READ); + accessControlService.checkPermission(user, Resource.AI_MODEL_SETTINGS, Operation.READ); validateSortProperty(sortProperty); var pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return aiSettingsService.findAiSettingsByTenantId(user.getTenantId(), pageLink); + return aiModelSettingsService.findAiModelSettingsByTenantId(user.getTenantId(), pageLink); } private static void validateSortProperty(String sortProperty) { @@ -120,31 +120,31 @@ public class AiSettingsController extends BaseController { } @ApiOperation( - value = "Delete AI settings by ID (deleteAiSettingsById)", - notes = "Deletes the AI settings record by its `id`. " + + value = "Delete AI model settings by ID (deleteAiModelSettingsById)", + notes = "Deletes the AI model settings record by its `id`. " + "If a record with the specified `id` exists, the record is deleted and the endpoint returns `true`. " + "If no such record exists, the endpoint returns `false`." + TENANT_AUTHORITY_PARAGRAPH ) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @DeleteMapping("/{aiSettingsId}") - public boolean deleteAiSettingsById( + @DeleteMapping("/{aiModelSettingsId}") + public boolean deleteAiModelSettingsById( @Parameter( - description = "ID of the AI settings record", + description = "ID of the AI model settings record", required = true, example = "de7900d4-30e2-11f0-9cd2-0242ac120002" ) - @PathVariable("aiSettingsId") UUID aiSettingsUuid + @PathVariable("aiModelSettingsId") UUID aiModelSettingsUuid ) throws ThingsboardException { var user = getCurrentUser(); - var aiSettingsId = new AiSettingsId(aiSettingsUuid); - accessControlService.checkPermission(user, Resource.AI_SETTINGS, Operation.DELETE); - Optional aiSettingsOpt = aiSettingsService.findAiSettingsByTenantIdAndId(user.getTenantId(), aiSettingsId); - if (aiSettingsOpt.isEmpty()) { + var settingsId = new AiModelSettingsId(aiModelSettingsUuid); + accessControlService.checkPermission(user, Resource.AI_MODEL_SETTINGS, Operation.DELETE); + Optional toDelete = aiModelSettingsService.findAiModelSettingsByTenantIdAndId(user.getTenantId(), settingsId); + if (toDelete.isEmpty()) { return false; } - accessControlService.checkPermission(user, Resource.AI_SETTINGS, Operation.DELETE, aiSettingsId, aiSettingsOpt.get()); - return tbAiSettingsService.delete(aiSettingsOpt.get(), user); + accessControlService.checkPermission(user, Resource.AI_MODEL_SETTINGS, Operation.DELETE, settingsId, toDelete.get()); + return tbAiModelSettingsService.delete(toDelete.get(), user); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 26ff7c1a94..179327f73b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -63,7 +63,7 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmInfo; @@ -78,7 +78,7 @@ import org.thingsboard.server.common.data.edge.EdgeInfo; import org.thingsboard.server.common.data.exception.EntityVersionMismatchException; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; @@ -131,7 +131,7 @@ import org.thingsboard.server.common.data.util.ThrowingBiFunction; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.dao.ai.AiSettingsService; +import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -177,7 +177,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.entitiy.TbLogEntityActionService; -import org.thingsboard.server.service.entitiy.ai.TbAiSettingsService; +import org.thingsboard.server.service.entitiy.ai.TbAiModelSettingsService; import org.thingsboard.server.service.entitiy.user.TbUserSettingsService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; @@ -378,10 +378,10 @@ public abstract class BaseController { protected CalculatedFieldService calculatedFieldService; @Autowired - protected AiSettingsService aiSettingsService; + protected AiModelSettingsService aiModelSettingsService; @Autowired - protected TbAiSettingsService tbAiSettingsService; + protected TbAiModelSettingsService tbAiModelSettingsService; @Value("${server.log_controller_error_stack_trace}") @Getter @@ -691,8 +691,8 @@ public abstract class BaseController { case CALCULATED_FIELD: checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation); return; - case AI_SETTINGS: - checkAiSettingsId(new AiSettingsId(entityId.getId()), operation); + case AI_MODEL_SETTINGS: + checkAiModelSettingsId(new AiModelSettingsId(entityId.getId()), operation); return; default: checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation); @@ -894,8 +894,8 @@ public abstract class BaseController { return checkEntityId(notificationTargetId, notificationTargetService::findNotificationTargetById, operation); } - AiSettings checkAiSettingsId(AiSettingsId aiSettingsId, Operation operation) throws ThingsboardException { - return checkEntityId(aiSettingsId, (tenantId, id) -> aiSettingsService.findAiSettingsByTenantIdAndId(tenantId, id).orElse(null), operation); + AiModelSettings checkAiModelSettingsId(AiModelSettingsId settingsId, Operation operation) throws ThingsboardException { + return checkEntityId(settingsId, (tenantId, id) -> aiModelSettingsService.findAiModelSettingsByTenantIdAndId(tenantId, id).orElse(null), operation); } protected I emptyId(EntityType entityType) { diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiModelServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/AiModelServiceImpl.java new file mode 100644 index 0000000000..728e46e922 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ai/AiModelServiceImpl.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ai; + +import dev.langchain4j.model.chat.ChatModel; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.rule.engine.api.RuleEngineAiModelService; +import org.thingsboard.server.common.data.ai.model.chat.AiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; + +@Service +@RequiredArgsConstructor +class AiModelServiceImpl implements RuleEngineAiModelService { + + private final Langchain4jChatModelConfigurer chatModelConfigurer; + + @Override + public > ChatModel configureChatModel(AiChatModel chatModel) { + return chatModel.configure(chatModelConfigurer); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java deleted file mode 100644 index eeddb5e6d2..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.ai; - -import dev.langchain4j.model.chat.ChatModel; -import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; -import dev.langchain4j.model.mistralai.MistralAiChatModel; -import dev.langchain4j.model.openai.OpenAiChatModel; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.thingsboard.rule.engine.api.RuleEngineAiService; -import org.thingsboard.server.common.data.ai.AiSettings; -import org.thingsboard.server.common.data.ai.model.AiModelConfig; -import org.thingsboard.server.common.data.ai.model.GoogleAiGeminiChatModelConfig; -import org.thingsboard.server.common.data.ai.model.MistralAiChatModelConfig; -import org.thingsboard.server.common.data.ai.model.OpenAiChatModelConfig; -import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; -import org.thingsboard.server.common.data.id.AiSettingsId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.ai.AiSettingsService; - -import java.time.Duration; -import java.util.NoSuchElementException; -import java.util.Optional; - -@Service -@RequiredArgsConstructor -class AiServiceImpl implements RuleEngineAiService { - - private final AiSettingsService aiSettingsService; - - @Override - public ChatModel configureChatModel(TenantId tenantId, AiSettingsId aiSettingsId) { - Optional aiSettingsOpt = aiSettingsService.findAiSettingsById(tenantId, aiSettingsId); - if (aiSettingsOpt.isEmpty()) { - throw new NoSuchElementException("AI settings with ID: " + aiSettingsId + " were not found"); - } - var aiSettings = aiSettingsOpt.get(); - return configureChatModel(aiSettings.getProviderConfig(), aiSettings.getModelConfig()); - } - - @Override - public ChatModel configureChatModel(AiProviderConfig providerConfig, AiModelConfig modelConfig) { - return switch (providerConfig.getProvider()) { - case OPENAI -> { - var modelBuilder = OpenAiChatModel.builder() - .apiKey(providerConfig.getApiKey()) - .modelName(modelConfig.getModel()); - - if (modelConfig instanceof OpenAiChatModelConfig config) { - modelBuilder.temperature(config.getTemperature()); - if (config.getTimeoutSeconds() != null) { - modelBuilder.timeout(Duration.ofSeconds(config.getTimeoutSeconds())); - } - modelBuilder.maxRetries(config.getMaxRetries()); - } - - yield modelBuilder.build(); - } - case MISTRAL_AI -> { - var modelBuilder = MistralAiChatModel.builder() - .apiKey(providerConfig.getApiKey()) - .modelName(modelConfig.getModel()); - - if (modelConfig instanceof MistralAiChatModelConfig config) { - modelBuilder.temperature(config.getTemperature()); - if (config.getTimeoutSeconds() != null) { - modelBuilder.timeout(Duration.ofSeconds(config.getTimeoutSeconds())); - } - modelBuilder.maxRetries(config.getMaxRetries()); - } - - yield modelBuilder.build(); - } - case GOOGLE_AI_GEMINI -> { - var modelBuilder = GoogleAiGeminiChatModel.builder() - .apiKey(providerConfig.getApiKey()) - .modelName(modelConfig.getModel()); - - if (modelConfig instanceof GoogleAiGeminiChatModelConfig config) { - modelBuilder.temperature(config.getTemperature()); - if (config.getTimeoutSeconds() != null) { - modelBuilder.timeout(Duration.ofSeconds(config.getTimeoutSeconds())); - } - modelBuilder.maxRetries(config.getMaxRetries()); - } - - yield modelBuilder.build(); - } - }; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java new file mode 100644 index 0000000000..f66f31400b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ai; + +import dev.langchain4j.model.chat.ChatModel; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; +import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModel; + +import java.time.Duration; + +@Component +public class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer { + + @Override + public ChatModel configureChatModel(OpenAiChatModel chatModel) { + OpenAiChatModel.Config modelConfig = chatModel.modelConfig(); + return dev.langchain4j.model.openai.OpenAiChatModel.builder() + .apiKey(chatModel.providerConfig().apiKey()) + .modelName(chatModel.modelId()) + .temperature(modelConfig.temperature()) + .timeout(toDuration(modelConfig.timeoutSeconds())) + .maxRetries(modelConfig.maxRetries()) + .build(); + } + + @Override + public ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel) { + GoogleAiGeminiChatModel.Config modelConfig = chatModel.modelConfig(); + return dev.langchain4j.model.googleai.GoogleAiGeminiChatModel.builder() + .apiKey(chatModel.providerConfig().apiKey()) + .modelName(chatModel.modelId()) + .temperature(modelConfig.temperature()) + .timeout(toDuration(modelConfig.timeoutSeconds())) + .maxRetries(modelConfig.maxRetries()) + .build(); + } + + @Override + public ChatModel configureChatModel(MistralAiChatModel chatModel) { + MistralAiChatModel.Config modelConfig = chatModel.modelConfig(); + return dev.langchain4j.model.mistralai.MistralAiChatModel.builder() + .apiKey(chatModel.providerConfig().apiKey()) + .modelName(chatModel.modelId()) + .temperature(modelConfig.temperature()) + .timeout(toDuration(modelConfig.timeoutSeconds())) + .maxRetries(modelConfig.maxRetries()) + .build(); + } + + private static Duration toDuration(Integer timeoutSeconds) { + return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index dc3d1a74f7..8bbed875fe 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -112,7 +112,7 @@ public class EdgeEventSourcingListener { return; } try { - if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_SETTINGS == entityType) { + if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL_SETTINGS == entityType) { return; } log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event); @@ -226,7 +226,7 @@ public class EdgeEventSourcingListener { break; case TENANT: return !event.getCreated(); - case API_USAGE_STATE, EDGE, AI_SETTINGS: + case API_USAGE_STATE, EDGE, AI_MODEL_SETTINGS: return false; case DOMAIN: if (entity instanceof Domain domain) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java index e0c24a13bc..5fca13380c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java @@ -68,7 +68,7 @@ public class RelatedEdgesSourcingListener { @TransactionalEventListener( fallbackExecution = true, - condition = "#event.entityId.getEntityType() != T(org.thingsboard.server.common.data.EntityType).AI_SETTINGS" + condition = "#event.entityId.getEntityType() != T(org.thingsboard.server.common.data.EntityType).AI_MODEL_SETTINGS" ) public void handleEvent(DeleteEntityEvent event) { executorService.submit(() -> { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index d0d9376f12..5823632b63 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -96,7 +96,7 @@ public class EntityStateSourcingListener { case ASSET -> { onAssetUpdate(event.getEntity(), event.getOldEntity()); } - case ASSET_PROFILE, ENTITY_VIEW, NOTIFICATION_RULE, AI_SETTINGS -> { + case ASSET_PROFILE, ENTITY_VIEW, NOTIFICATION_RULE, AI_MODEL_SETTINGS -> { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, lifecycleEvent); } case RULE_CHAIN -> { @@ -158,7 +158,7 @@ public class EntityStateSourcingListener { Asset asset = (Asset) event.getEntity(); tbClusterService.onAssetDeleted(tenantId, asset, null); } - case ASSET_PROFILE, ENTITY_VIEW, CUSTOMER, EDGE, NOTIFICATION_RULE, AI_SETTINGS -> { + case ASSET_PROFILE, ENTITY_VIEW, CUSTOMER, EDGE, NOTIFICATION_RULE, AI_MODEL_SETTINGS -> { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, ComponentLifecycleEvent.DELETED); } case NOTIFICATION_REQUEST -> { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiSettingsService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java similarity index 60% rename from application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiSettingsService.java rename to application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java index 08e663633d..5a41d83eed 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java @@ -19,9 +19,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.dao.ai.AiSettingsService; +import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @@ -30,22 +30,22 @@ import static java.util.Objects.requireNonNullElseGet; @Service @TbCoreComponent @RequiredArgsConstructor -class DefaultTbAiSettingsService extends AbstractTbEntityService implements TbAiSettingsService { +class DefaultTbAiModelSettingsService extends AbstractTbEntityService implements TbAiModelSettingsService { - private final AiSettingsService aiSettingsService; + private final AiModelSettingsService aiModelSettingsService; @Override - public AiSettings save(AiSettings aiSettings, User user) { - var actionType = aiSettings.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + public AiModelSettings save(AiModelSettings settings, User user) { + var actionType = settings.getId() == null ? ActionType.ADDED : ActionType.UPDATED; var tenantId = user.getTenantId(); - aiSettings.setTenantId(tenantId); + settings.setTenantId(tenantId); - AiSettings savedSettings; + AiModelSettings savedSettings; try { - savedSettings = aiSettingsService.save(aiSettings); + savedSettings = aiModelSettingsService.save(settings); } catch (Exception e) { - logEntityActionService.logEntityAction(tenantId, requireNonNullElseGet(aiSettings.getId(), () -> emptyId(EntityType.AI_SETTINGS)), aiSettings, actionType, user, e); + logEntityActionService.logEntityAction(tenantId, requireNonNullElseGet(settings.getId(), () -> emptyId(EntityType.AI_MODEL_SETTINGS)), settings, actionType, user, e); throw e; } @@ -55,22 +55,22 @@ class DefaultTbAiSettingsService extends AbstractTbEntityService implements TbAi } @Override - public boolean delete(AiSettings aiSettings, User user) { + public boolean delete(AiModelSettings settings, User user) { var actionType = ActionType.DELETED; var tenantId = user.getTenantId(); - var aiSettingsId = aiSettings.getId(); + var settingsId = settings.getId(); boolean deleted; try { - deleted = aiSettingsService.deleteByTenantIdAndId(tenantId, aiSettingsId); + deleted = aiModelSettingsService.deleteByTenantIdAndId(tenantId, settingsId); } catch (Exception e) { - logEntityActionService.logEntityAction(tenantId, aiSettingsId, aiSettings, actionType, user, e, aiSettingsId.toString()); + logEntityActionService.logEntityAction(tenantId, settingsId, settings, actionType, user, e, settingsId.toString()); throw e; } if (deleted) { - logEntityActionService.logEntityAction(tenantId, aiSettingsId, aiSettings, actionType, user, aiSettingsId.toString()); + logEntityActionService.logEntityAction(tenantId, settingsId, settings, actionType, user, settingsId.toString()); } return deleted; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiSettingsService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelSettingsService.java similarity index 76% rename from application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiSettingsService.java rename to application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelSettingsService.java index 399a3b2bdd..0d66c171a7 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelSettingsService.java @@ -16,12 +16,12 @@ package org.thingsboard.server.service.entitiy.ai; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.AiModelSettings; -public interface TbAiSettingsService { +public interface TbAiModelSettingsService { - AiSettings save(AiSettings aiSettings, User user); + AiModelSettings save(AiModelSettings settings, User user); - boolean delete(AiSettings aiSettings, User user); + boolean delete(AiModelSettings settings, User user); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index aa27b4592b..2a2a39c4d2 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -590,7 +590,7 @@ public class DefaultTbClusterService implements TbClusterService { EntityType.ENTITY_VIEW, EntityType.NOTIFICATION_RULE, EntityType.CALCULATED_FIELD, - EntityType.AI_SETTINGS, + EntityType.AI_MODEL_SETTINGS, EntityType.TENANT_PROFILE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE) diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index 55b7577ebe..cdbd2bccec 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -52,7 +52,7 @@ public enum Resource { EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE), MOBILE_APP_SETTINGS, CALCULATED_FIELD(EntityType.CALCULATED_FIELD), - AI_SETTINGS(EntityType.AI_SETTINGS); + AI_MODEL_SETTINGS(EntityType.AI_MODEL_SETTINGS); private final Set entityTypes; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index 990f29798c..49c4350761 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -18,8 +18,8 @@ package org.thingsboard.server.service.security.permission; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.ai.AiSettings; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; @@ -58,7 +58,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.MOBILE_APP, tenantEntityPermissionChecker); put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker); put(Resource.CALCULATED_FIELD, tenantEntityPermissionChecker); - put(Resource.AI_SETTINGS, aiSettingsPermissionChecker); + put(Resource.AI_MODEL_SETTINGS, aiModelSettingsPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { @@ -149,7 +149,7 @@ public class TenantAdminPermissions extends AbstractPermissions { }; - private static final PermissionChecker aiSettingsPermissionChecker = new PermissionChecker<>() { + private static final PermissionChecker aiModelSettingsPermissionChecker = new PermissionChecker<>() { @Override public boolean hasPermission(SecurityUser user, Operation operation) { @@ -157,7 +157,7 @@ public class TenantAdminPermissions extends AbstractPermissions { } @Override - public boolean hasPermission(SecurityUser user, Operation operation, AiSettingsId entityId, AiSettings entity) { + public boolean hasPermission(SecurityUser user, Operation operation, AiModelSettingsId entityId, AiModelSettings entity) { return user.getTenantId().equals(entity.getTenantId()); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index dd41b13de0..f79a387b58 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -657,9 +657,9 @@ cache: trendzSettings: timeToLiveInMinutes: "${CACHE_SPECS_TRENDZ_SETTINGS_TTL:1440}" # Trendz settings cache TTL maxSize: "${CACHE_SPECS_TRENDZ_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled - aiSettings: - timeToLiveInMinutes: "${CACHE_SPECS_AI_SETTINGS_TTL:1440}" # AI settings cache TTL - maxSize: "${CACHE_SPECS_AI_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled + aiModelSettings: + timeToLiveInMinutes: "${CACHE_SPECS_AI_MODEL_SETTINGS_TTL:1440}" # AI model settings cache TTL + maxSize: "${CACHE_SPECS_AI_MODEL_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled # Deliberately placed outside the 'specs' group above notificationRules: @@ -875,7 +875,7 @@ audit-log: "tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels. "ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package logging levels. "calculated_field": "${AUDIT_LOG_MASK_CALCULATED_FIELD:W}" # Calculated field logging levels. - "ai_settings": "${AUDIT_LOG_MASK_AI_SETTINGS:W}" # AI settings logging levels. + "ai_model_settings": "${AUDIT_LOG_MASK_AI_MODEL_SETTINGS:W}" # AI model settings logging levels. sink: # Type of external sink. possible options: none, elasticsearch type: "${AUDIT_LOG_SINK_TYPE:none}" diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsService.java similarity index 55% rename from common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsService.java index 83a66de876..09219f238e 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsService.java @@ -16,8 +16,8 @@ package org.thingsboard.server.dao.ai; import com.google.common.util.concurrent.FluentFuture; -import org.thingsboard.server.common.data.ai.AiSettings; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -25,18 +25,18 @@ import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.Optional; -public interface AiSettingsService extends EntityDaoService { +public interface AiModelSettingsService extends EntityDaoService { - AiSettings save(AiSettings aiSettings); + AiModelSettings save(AiModelSettings settings); - Optional findAiSettingsById(TenantId tenantId, AiSettingsId aiSettingsId); + Optional findAiModelSettingsById(TenantId tenantId, AiModelSettingsId settingsId); - PageData findAiSettingsByTenantId(TenantId tenantId, PageLink pageLink); + PageData findAiModelSettingsByTenantId(TenantId tenantId, PageLink pageLink); - Optional findAiSettingsByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId); + Optional findAiModelSettingsByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); - FluentFuture> findAiSettingsByTenantIdAndIdAsync(TenantId tenantId, AiSettingsId aiSettingsId); + FluentFuture> findAiModelSettingsByTenantIdAndIdAsync(TenantId tenantId, AiModelSettingsId settingsId); - boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId); + boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); } diff --git a/common/data/pom.xml b/common/data/pom.xml index 648f3d1086..beb67c6b19 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -112,6 +112,10 @@ leshan-core compile + + dev.langchain4j + langchain4j + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 4b103d769a..c5df7c10c0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -37,7 +37,7 @@ public final class CacheConstants { public static final String NOTIFICATION_SETTINGS_CACHE = "notificationSettings"; public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications"; public static final String TRENDZ_SETTINGS_CACHE = "trendzSettings"; - public static final String AI_SETTINGS_CACHE = "aiSettings"; + public static final String AI_MODEL_SETTINGS_CACHE = "aiModelSettings"; public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ATTRIBUTES_CACHE = "attributes"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 89bc456fd9..868b36f73b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -64,10 +64,10 @@ public enum EntityType { MOBILE_APP_BUNDLE(38), CALCULATED_FIELD(39), CALCULATED_FIELD_LINK(40), - AI_SETTINGS(41, "ai_settings") { + AI_MODEL_SETTINGS(41, "ai_model_settings") { @Override public String getNormalName() { - return "AI settings"; + return "AI model settings"; } }; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java similarity index 52% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java index d38ac45992..df1dbe4d7e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java @@ -24,10 +24,8 @@ import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasVersion; -import org.thingsboard.server.common.data.ai.model.AiModelConfig; -import org.thingsboard.server.common.data.ai.provider.AiProvider; -import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.ai.model.AiModel; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.TenantId; import java.io.Serial; @@ -36,7 +34,7 @@ import java.io.Serial; @Builder @AllArgsConstructor @EqualsAndHashCode(callSuper = true) -public final class AiSettings extends BaseData implements HasTenantId, HasVersion, HasName { +public final class AiModelSettings extends BaseData implements HasTenantId, HasVersion, HasName { @Serial private static final long serialVersionUID = 9017108678716011604L; @@ -44,7 +42,7 @@ public final class AiSettings extends BaseData implements HasTenan @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, - description = "JSON object representing the ID of the tenant associated with these AI settings", + description = "JSON object representing the ID of the tenant associated with these AI model settings", example = "e3c4b7d2-5678-4a9b-0c1d-2e3f4a5b6c7d" ) TenantId tenantId; @@ -52,7 +50,7 @@ public final class AiSettings extends BaseData implements HasTenan @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, - description = "Version of the AI settings; increments automatically whenever the settings are changed", + description = "Version of the AI model settings; increments automatically whenever the settings are changed", example = "7", defaultValue = "1" ) @@ -61,49 +59,21 @@ public final class AiSettings extends BaseData implements HasTenan @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_WRITE, - description = "Human-readable name of the AI settings; must be unique within the scope of the tenant", + description = "Human-readable name of the AI model settings; must be unique within the scope of the tenant", example = "Default AI Settings" ) String name; - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Name of the AI provider", - example = "OPENAI", - allowableValues = {"OPENAI", "GOOGLE_AI_GEMINI", "MISTRAL_AI"}, - type = "string" - ) - AiProvider provider; - - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Configuration specific to the AI provider" - ) - AiProviderConfig providerConfig; - - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Identifier of the AI model", - example = "gpt-4o-mini" - ) - String model; - @Schema( requiredMode = Schema.RequiredMode.NOT_REQUIRED, accessMode = Schema.AccessMode.READ_WRITE, - description = """ - Optional configuration specific to the AI model. - If provided, it must be one of the known `AiModelConfig` subtypes and any settings - you specify will override the model’s defaults; if omitted, the model will run with its built-in defaults.""" + description = "Configuration of the AI model" ) - AiModelConfig modelConfig; + AiModel configuration; - public AiSettings() {} + public AiModelSettings() {} - public AiSettings(AiSettingsId id) { + public AiModelSettings(AiModelSettingsId id) { super(id); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java new file mode 100644 index 0000000000..686c1f4fcf --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModel; +import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "modelId", + visible = true +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4o"), + @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.5-flash"), + @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "mistral-medium-latest") +}) +public interface AiModel> { + + AiProviderConfig providerConfig(); + + AiModelType modelType(); + + String modelId(); + + C modelConfig(); + + AiModel withModelConfig(C config); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java index 779b75812f..b07007c301 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java @@ -15,42 +15,4 @@ */ package org.thingsboard.server.common.data.ai.model; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "model", - visible = true -) -@JsonSubTypes({ - @JsonSubTypes.Type(value = OpenAiChatModelConfig.class, name = "gpt-4o"), - @JsonSubTypes.Type(value = OpenAiChatModelConfig.class, name = "gpt-4o-mini"), - @JsonSubTypes.Type(value = GoogleAiGeminiChatModelConfig.class, name = "gemini-2.0-flash"), - @JsonSubTypes.Type(value = MistralAiChatModelConfig.class, name = "mistral-medium-latest") -}) -public abstract class AiModelConfig { - - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Identifier of the AI model" - ) - private String model; - - public abstract Integer getTimeoutSeconds(); - - public abstract void setTimeoutSeconds(Integer timeoutSeconds); - - public abstract Integer getMaxRetries(); - - public abstract void setMaxRetries(Integer timeoutSeconds); - - -} +public interface AiModelConfig> {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelType.java new file mode 100644 index 0000000000..d6299cf5e6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelType.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model; + +public enum AiModelType { + + CHAT + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/GoogleAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/GoogleAiGeminiChatModelConfig.java deleted file mode 100644 index 1d85115f31..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/GoogleAiGeminiChatModelConfig.java +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.common.data.ai.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -@Data -@EqualsAndHashCode(callSuper = true) -@NoArgsConstructor -@Schema( - name = "GoogleAiGeminiChatModelConfig", - description = "Configuration for Google AI Gemini chat models" -) -public final class GoogleAiGeminiChatModelConfig extends AiModelConfig { - - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Identifier of the AI model", - allowableValues = "gemini-2.0-flash", - example = "gemini-2.0-flash" - ) - public String getModel() { - return super.getModel(); - } - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Sampling temperature to control randomness: 0.0 (most deterministic) to 1.0 (most creative)", - example = "0.7" - ) - private Double temperature; - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Timeout (in seconds) for establishing HTTP connection" - ) - private Integer timeoutSeconds; - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Maximum number of times to retry an LLM call upon exception (except for non-retriable ones like authentication or invalid request errors)" - ) - private Integer maxRetries; - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/MistralAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/MistralAiChatModelConfig.java deleted file mode 100644 index d333eb6283..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/MistralAiChatModelConfig.java +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.common.data.ai.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -@Data -@EqualsAndHashCode(callSuper = true) -@NoArgsConstructor -@Schema( - name = "MistralAiChatModelConfig", - description = "Configuration for Mistral AI chat models" -) -public final class MistralAiChatModelConfig extends AiModelConfig { - - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Identifier of the AI model", - allowableValues = "mistral-medium-latest", - example = "mistral-medium-latest" - ) - public String getModel() { - return super.getModel(); - } - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Sampling temperature to control randomness: 0.0 (most deterministic) to 1.0 (most creative)", - example = "0.7" - ) - private Double temperature; - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Timeout (in seconds) for the entire HTTP call: applied to connect, read, and write operations" - ) - private Integer timeoutSeconds; - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Maximum number of times to retry an LLM call upon exception (except for non-retriable ones like authentication or invalid request errors)" - ) - private Integer maxRetries; - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/OpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/OpenAiChatModelConfig.java deleted file mode 100644 index accf5c92ec..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/OpenAiChatModelConfig.java +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.common.data.ai.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -@Data -@EqualsAndHashCode(callSuper = true) -@NoArgsConstructor -@Schema( - name = "OpenAiChatModelConfig", - description = "Configuration for OpenAI chat models" -) -public final class OpenAiChatModelConfig extends AiModelConfig { - - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Identifier of the AI model", - allowableValues = {"gpt-4o", "gpt-4o-mini"}, - example = "gpt-4o" - ) - public String getModel() { - return super.getModel(); - } - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Sampling temperature to control randomness: 0.0 (most deterministic) to 1.0 (most creative)", - example = "0.7" - ) - private Double temperature; - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Timeout (in seconds) for both establishing HTTP connection and receiving a response" - ) - private Integer timeoutSeconds; - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Maximum number of times to retry an LLM call upon exception (except for non-retriable ones like authentication or invalid request errors)" - ) - private Integer maxRetries; - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java new file mode 100644 index 0000000000..17f79d93e2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.model.AiModel; +import org.thingsboard.server.common.data.ai.model.AiModelType; + +public sealed interface AiChatModel> extends AiModel + permits OpenAiChatModel, GoogleAiGeminiChatModel, MistralAiChatModel { + + ChatModel configure(Langchain4jChatModelConfigurer configurer); + + @Override + default AiModelType modelType() { + return AiModelType.CHAT; + } + + @Override + C modelConfig(); + + @Override + AiChatModel withModelConfig(C config); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java new file mode 100644 index 0000000000..565923cf6c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import org.thingsboard.server.common.data.ai.model.AiModelConfig; + +public sealed interface AiChatModelConfig> extends AiModelConfig + permits OpenAiChatModel.Config, GoogleAiGeminiChatModel.Config, MistralAiChatModel.Config { + + Double temperature(); + + Integer timeoutSeconds(); + + Integer maxRetries(); + + C withTemperature(Double temperature); + + C withTimeoutSeconds(Integer timeoutSeconds); + + C withMaxRetries(Integer maxRetries); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java new file mode 100644 index 0000000000..875262abb6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; + +public record GoogleAiGeminiChatModel( + GoogleAiGeminiProviderConfig providerConfig, + String modelId, + Config modelConfig +) implements AiChatModel { + + public record Config( + Double temperature, + Integer timeoutSeconds, + Integer maxRetries + ) implements AiChatModelConfig { + + @Override + public Config withTemperature(Double temperature) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + @Override + public Config withTimeoutSeconds(Integer timeoutSeconds) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + @Override + public Config withMaxRetries(Integer maxRetries) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public GoogleAiGeminiChatModel withModelConfig(GoogleAiGeminiChatModel.Config config) { + return new GoogleAiGeminiChatModel(providerConfig, modelId, config); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java new file mode 100644 index 0000000000..87d317eb0b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; + +public interface Langchain4jChatModelConfigurer { + + ChatModel configureChatModel(OpenAiChatModel chatModel); + + ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel); + + ChatModel configureChatModel(MistralAiChatModel chatModel); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java new file mode 100644 index 0000000000..413d2b93a8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; + +public record MistralAiChatModel( + MistralAiProviderConfig providerConfig, + String modelId, + Config modelConfig +) implements AiChatModel { + + public record Config( + Double temperature, + Integer timeoutSeconds, + Integer maxRetries + ) implements AiChatModelConfig { + + @Override + public Config withTemperature(Double temperature) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + @Override + public Config withTimeoutSeconds(Integer timeoutSeconds) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + @Override + public Config withMaxRetries(Integer maxRetries) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public MistralAiChatModel withModelConfig(Config config) { + return new MistralAiChatModel(providerConfig, modelId, config); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java new file mode 100644 index 0000000000..0d5031d512 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; + +public record OpenAiChatModel( + OpenAiProviderConfig providerConfig, + String modelId, + Config modelConfig +) implements AiChatModel { + + public record Config( + Double temperature, + Integer timeoutSeconds, + Integer maxRetries + ) implements AiChatModelConfig { + + @Override + public OpenAiChatModel.Config withTemperature(Double temperature) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + @Override + public OpenAiChatModel.Config withTimeoutSeconds(Integer timeoutSeconds) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + @Override + public OpenAiChatModel.Config withMaxRetries(Integer maxRetries) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public OpenAiChatModel withModelConfig(OpenAiChatModel.Config config) { + return new OpenAiChatModel(providerConfig, modelId, config); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java index f350ed75ec..b7c594ba60 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java @@ -17,32 +17,22 @@ package org.thingsboard.server.common.data.ai.provider; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; -@Data -@NoArgsConstructor @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "provider", - visible = true + include = JsonTypeInfo.As.PROPERTY, + property = "provider" ) @JsonSubTypes({ @JsonSubTypes.Type(value = OpenAiProviderConfig.class, name = "OPENAI"), @JsonSubTypes.Type(value = GoogleAiGeminiProviderConfig.class, name = "GOOGLE_AI_GEMINI"), @JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI") }) -public abstract class AiProviderConfig { +public sealed interface AiProviderConfig + permits OpenAiProviderConfig, GoogleAiGeminiProviderConfig, MistralAiProviderConfig { - public abstract AiProvider getProvider(); + AiProvider provider(); - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "API key for authenticating with the AI provider" - ) - private String apiKey; + String apiKey(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java index 9ca8643173..0bb9d21b52 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java @@ -15,26 +15,16 @@ */ package org.thingsboard.server.common.data.ai.provider; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; +public record GoogleAiGeminiProviderConfig(String apiKey) implements AiProviderConfig { -@Data -@EqualsAndHashCode(callSuper = true) -@Schema( - name = "GoogleAiGeminiProviderConfig", - description = "Configuration for the Google AI Gemini provider" -) -public final class GoogleAiGeminiProviderConfig extends AiProviderConfig { + @Override + public AiProvider provider() { + return AiProvider.GOOGLE_AI_GEMINI; + } - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Name of the AI provider", - example = "GOOGLE_AI_GEMINI", - allowableValues = "GOOGLE_AI_GEMINI", - type = "string" - ) - private AiProvider provider = AiProvider.GOOGLE_AI_GEMINI; + @Override + public String apiKey() { + return apiKey; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java index bc17f34220..45e3b68800 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java @@ -15,26 +15,16 @@ */ package org.thingsboard.server.common.data.ai.provider; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; +public record MistralAiProviderConfig(String apiKey) implements AiProviderConfig { -@Data -@EqualsAndHashCode(callSuper = true) -@Schema( - name = "MistralAiProviderConfig", - description = "Configuration for the Mistral AI provider" -) -public final class MistralAiProviderConfig extends AiProviderConfig { + @Override + public AiProvider provider() { + return AiProvider.MISTRAL_AI; + } - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Name of the AI provider", - example = "MISTRAL_AI", - allowableValues = "MISTRAL_AI", - type = "string" - ) - private AiProvider provider = AiProvider.MISTRAL_AI; + @Override + public String apiKey() { + return apiKey; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java index 36ac8b4b35..0536d1176e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java @@ -15,26 +15,16 @@ */ package org.thingsboard.server.common.data.ai.provider; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; +public record OpenAiProviderConfig(String apiKey) implements AiProviderConfig { -@Data -@EqualsAndHashCode(callSuper = true) -@Schema( - name = "OpenAiProviderConfig", - description = "Configuration for the OpenAI provider" -) -public final class OpenAiProviderConfig extends AiProviderConfig { + @Override + public AiProvider provider() { + return AiProvider.OPENAI; + } - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Name of the AI provider", - example = "OPENAI", - allowableValues = "OPENAI", - type = "string" - ) - private AiProvider provider = AiProvider.OPENAI; + @Override + public String apiKey() { + return apiKey; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelSettingsId.java similarity index 72% rename from common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java rename to common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelSettingsId.java index 8aac27051a..83b4fefab2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelSettingsId.java @@ -23,29 +23,29 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; -public final class AiSettingsId extends UUIDBased implements EntityId { +public final class AiModelSettingsId extends UUIDBased implements EntityId { @Serial private static final long serialVersionUID = 3021036138554389754L; @JsonCreator - public AiSettingsId(@JsonProperty("id") UUID id) { + public AiModelSettingsId(@JsonProperty("id") UUID id) { super(id); } @Override @Schema( requiredMode = Schema.RequiredMode.REQUIRED, - description = "Entity type of the AI settings", - example = "AI_SETTINGS", - allowableValues = "AI_SETTINGS" + description = "Entity type of the AI model settings", + example = "AI_MODEL_SETTINGS", + allowableValues = "AI_MODEL_SETTINGS" ) public EntityType getEntityType() { - return EntityType.AI_SETTINGS; + return EntityType.AI_MODEL_SETTINGS; } - public static AiSettingsId fromString(String uuid) { - return new AiSettingsId(UUID.fromString(uuid)); + public static AiModelSettingsId fromString(String uuid) { + return new AiModelSettingsId(UUID.fromString(uuid)); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index fcdf1e0a1b..0738ce45ee 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -84,7 +84,7 @@ public class EntityIdFactory { case MOBILE_APP_BUNDLE -> new MobileAppBundleId(uuid); case CALCULATED_FIELD -> new CalculatedFieldId(uuid); case CALCULATED_FIELD_LINK -> new CalculatedFieldLinkId(uuid); - case AI_SETTINGS -> new AiSettingsId(uuid); + case AI_MODEL_SETTINGS -> new AiModelSettingsId(uuid); default -> throw new IllegalArgumentException("EntityType " + type + " is not supported!"); }; } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index e5a94c21fc..40f154d276 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -63,7 +63,7 @@ enum EntityTypeProto { MOBILE_APP_BUNDLE = 38; CALCULATED_FIELD = 39; CALCULATED_FIELD_LINK = 40; - AI_SETTINGS = 41; + AI_MODEL_SETTINGS = 41; } enum ApiUsageRecordKeyProto { diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheEvictEvent.java similarity index 68% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheEvictEvent.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheEvictEvent.java index d6cad8bf16..16ce64256b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheEvictEvent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheEvictEvent.java @@ -15,15 +15,15 @@ */ package org.thingsboard.server.dao.ai; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.TenantId; import java.util.Set; -record AiSettingsCacheEvictEvent(Set keys) { +record AiModelSettingsCacheEvictEvent(Set keys) { - static AiSettingsCacheEvictEvent of(TenantId tenantId, AiSettingsId aiSettingsId) { - return new AiSettingsCacheEvictEvent(Set.of(AiSettingsCacheKey.of(tenantId, aiSettingsId))); + static AiModelSettingsCacheEvictEvent of(TenantId tenantId, AiModelSettingsId settingsId) { + return new AiModelSettingsCacheEvictEvent(Set.of(AiModelSettingsCacheKey.of(tenantId, settingsId))); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheKey.java similarity index 70% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheKey.java index d732c08fc8..9d73289be7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheKey.java @@ -17,22 +17,22 @@ package org.thingsboard.server.dao.ai; import org.checkerframework.checker.nullness.qual.NonNull; import org.thingsboard.server.cache.VersionedCacheKey; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.TenantId; import java.util.UUID; import static java.util.Objects.requireNonNull; -record AiSettingsCacheKey(UUID tenantId, UUID aiSettingsId) implements VersionedCacheKey { +record AiModelSettingsCacheKey(UUID tenantId, UUID settingsId) implements VersionedCacheKey { - AiSettingsCacheKey { + AiModelSettingsCacheKey { requireNonNull(tenantId); - requireNonNull(aiSettingsId); + requireNonNull(settingsId); } - static AiSettingsCacheKey of(TenantId tenantId, AiSettingsId aiSettingsId) { - return new AiSettingsCacheKey(tenantId.getId(), aiSettingsId.getId()); + static AiModelSettingsCacheKey of(TenantId tenantId, AiModelSettingsId settingsId) { + return new AiModelSettingsCacheKey(tenantId.getId(), settingsId.getId()); } @Override @@ -43,7 +43,7 @@ record AiSettingsCacheKey(UUID tenantId, UUID aiSettingsId) implements Versioned @NonNull @Override public String toString() { - return /* cache name */ "_" + tenantId + "_" + aiSettingsId; + return /* cache name */ "_" + tenantId + "_" + settingsId; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCaffeineCache.java similarity index 75% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCaffeineCache.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCaffeineCache.java index f006c0a757..d758cb21c5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCaffeineCache.java @@ -20,14 +20,14 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Component; import org.thingsboard.server.cache.VersionedCaffeineTbCache; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.AiModelSettings; -@Component("AiSettingsCache") +@Component("AiModelSettingsCache") @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) -class AiSettingsCaffeineCache extends VersionedCaffeineTbCache { +class AiModelSettingsCaffeineCache extends VersionedCaffeineTbCache { - AiSettingsCaffeineCache(CacheManager cacheManager) { - super(cacheManager, CacheConstants.AI_SETTINGS_CACHE); + AiModelSettingsCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.AI_MODEL_SETTINGS_CACHE); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java similarity index 64% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsDao.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java index 94a096d679..ae47af0db2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java @@ -15,22 +15,22 @@ */ package org.thingsboard.server.dao.ai; -import org.thingsboard.server.common.data.ai.AiSettings; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.TenantEntityDao; import java.util.Optional; -public interface AiSettingsDao extends Dao, TenantEntityDao { +public interface AiModelSettingsDao extends Dao, TenantEntityDao { - Optional findByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId); + Optional findByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); - boolean deleteById(TenantId tenantId, AiSettingsId aiSettingsId); + boolean deleteById(TenantId tenantId, AiModelSettingsId settingsId); int deleteByTenantId(TenantId tenantId); - boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId); + boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsRedisCache.java similarity index 73% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsRedisCache.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsRedisCache.java index 0c1da26769..8674c522ff 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsRedisCache.java @@ -23,14 +23,14 @@ import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.cache.TbJsonRedisSerializer; import org.thingsboard.server.cache.VersionedRedisTbCache; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.AiModelSettings; @Component("AiSettingsCache") @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") -class AiSettingsRedisCache extends VersionedRedisTbCache { +class AiModelSettingsRedisCache extends VersionedRedisTbCache { - AiSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { - super(CacheConstants.AI_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(AiSettings.class)); + AiModelSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.AI_MODEL_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(AiModelSettings.class)); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java similarity index 50% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java index 8950e3559f..2fa47bf60f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java @@ -22,8 +22,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ai.AiSettings; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; @@ -43,29 +43,29 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink; @Service @RequiredArgsConstructor -class AiSettingsServiceImpl extends CachedVersionedEntityService implements AiSettingsService { +class AiModelSettingsServiceImpl extends CachedVersionedEntityService implements AiModelSettingsService { - private final DataValidator aiSettingsValidator; + private final DataValidator aiModelSettingsValidator; private final JpaExecutorService jpaExecutor; - private final AiSettingsDao aiSettingsDao; + private final AiModelSettingsDao aiModelSettingsDao; @Override @TransactionalEventListener - public void handleEvictEvent(AiSettingsCacheEvictEvent event) { + public void handleEvictEvent(AiModelSettingsCacheEvictEvent event) { cache.evict(event.keys()); } @Override @Transactional - public AiSettings save(AiSettings aiSettings) { - AiSettings oldSettings = aiSettingsValidator.validate(aiSettings, AiSettings::getTenantId); + public AiModelSettings save(AiModelSettings settings) { + AiModelSettings oldSettings = aiModelSettingsValidator.validate(settings, AiModelSettings::getTenantId); - AiSettings savedSettings; + AiModelSettings savedSettings; try { - savedSettings = aiSettingsDao.saveAndFlush(aiSettings.getTenantId(), aiSettings); + savedSettings = aiModelSettingsDao.saveAndFlush(settings.getTenantId(), settings); } catch (Exception e) { - checkConstraintViolation(e, "ai_settings_name_unq_key", "AI settings record with such name already exists!"); + checkConstraintViolation(e, "ai_model_settings_name_unq_key", "AI model settings with such name already exist!"); throw e; } @@ -78,65 +78,65 @@ class AiSettingsServiceImpl extends CachedVersionedEntityService findAiSettingsById(TenantId tenantId, AiSettingsId aiSettingsId) { - return Optional.ofNullable(aiSettingsDao.findById(tenantId, aiSettingsId.getId())); + public Optional findAiModelSettingsById(TenantId tenantId, AiModelSettingsId settingsId) { + return Optional.ofNullable(aiModelSettingsDao.findById(tenantId, settingsId.getId())); } @Override - public PageData findAiSettingsByTenantId(TenantId tenantId, PageLink pageLink) { + public PageData findAiModelSettingsByTenantId(TenantId tenantId, PageLink pageLink) { validatePageLink(pageLink); - return aiSettingsDao.findAllByTenantId(tenantId, pageLink); + return aiModelSettingsDao.findAllByTenantId(tenantId, pageLink); } @Override - public Optional findAiSettingsByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { - var cacheKey = AiSettingsCacheKey.of(tenantId, aiSettingsId); - return Optional.ofNullable(cache.get(cacheKey, () -> aiSettingsDao.findByTenantIdAndId(tenantId, aiSettingsId).orElse(null))); + public Optional findAiModelSettingsByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) { + var cacheKey = AiModelSettingsCacheKey.of(tenantId, settingsId); + return Optional.ofNullable(cache.get(cacheKey, () -> aiModelSettingsDao.findByTenantIdAndId(tenantId, settingsId).orElse(null))); } @Override - public FluentFuture> findAiSettingsByTenantIdAndIdAsync(TenantId tenantId, AiSettingsId aiSettingsId) { - return FluentFuture.from(jpaExecutor.submit(() -> findAiSettingsByTenantIdAndId(tenantId, aiSettingsId))); + public FluentFuture> findAiModelSettingsByTenantIdAndIdAsync(TenantId tenantId, AiModelSettingsId settingsId) { + return FluentFuture.from(jpaExecutor.submit(() -> findAiModelSettingsByTenantIdAndId(tenantId, settingsId))); } @Override @Transactional - public boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { - return deleteByTenantIdAndIdInternal(tenantId, aiSettingsId); + public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) { + return deleteByTenantIdAndIdInternal(tenantId, settingsId); } @Override public Optional> findEntity(TenantId tenantId, EntityId entityId) { - return findAiSettingsByTenantIdAndId(tenantId, (AiSettingsId) entityId) - .map(aiSettings -> aiSettings); // necessary to cast to HasId + return findAiModelSettingsByTenantIdAndId(tenantId, (AiModelSettingsId) entityId) + .map(settings -> settings); // necessary to cast to HasId } @Override public long countByTenantId(TenantId tenantId) { - return aiSettingsDao.countByTenantId(tenantId); + return aiModelSettingsDao.countByTenantId(tenantId); } @Override @Transactional public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { - deleteByTenantIdAndIdInternal(tenantId, new AiSettingsId(id.getId())); + deleteByTenantIdAndIdInternal(tenantId, new AiModelSettingsId(id.getId())); } - private boolean deleteByTenantIdAndIdInternal(TenantId tenantId, AiSettingsId aiSettingsId) { - Optional aiSettingsOpt = aiSettingsDao.findByTenantIdAndId(tenantId, aiSettingsId); - if (aiSettingsOpt.isEmpty()) { + private boolean deleteByTenantIdAndIdInternal(TenantId tenantId, AiModelSettingsId settingsId) { + Optional toDeleteOpt = aiModelSettingsDao.findByTenantIdAndId(tenantId, settingsId); + if (toDeleteOpt.isEmpty()) { return false; } - boolean deleted = aiSettingsDao.deleteByTenantIdAndId(tenantId, aiSettingsId); + boolean deleted = aiModelSettingsDao.deleteByTenantIdAndId(tenantId, settingsId); if (deleted) { - publishDeleteEvent(aiSettingsOpt.get()); - publishEvictEvent(AiSettingsCacheEvictEvent.of(tenantId, aiSettingsId)); + publishDeleteEvent(toDeleteOpt.get()); + publishEvictEvent(AiModelSettingsCacheEvictEvent.of(tenantId, settingsId)); } return deleted; } @@ -144,23 +144,23 @@ class AiSettingsServiceImpl extends CachedVersionedEntityService deletedSettings = aiSettingsDao.findAllByTenantId(tenantId, new PageLink(Integer.MAX_VALUE)).getData(); - if (deletedSettings.isEmpty()) { + List toDelete = aiModelSettingsDao.findAllByTenantId(tenantId, new PageLink(Integer.MAX_VALUE)).getData(); + if (toDelete.isEmpty()) { return; } - aiSettingsDao.deleteByTenantId(tenantId); + aiModelSettingsDao.deleteByTenantId(tenantId); - Set cacheKeys = Sets.newHashSetWithExpectedSize(deletedSettings.size()); - deletedSettings.forEach(settings -> { + Set cacheKeys = Sets.newHashSetWithExpectedSize(toDelete.size()); + toDelete.forEach(settings -> { publishDeleteEvent(settings); - cacheKeys.add(AiSettingsCacheKey.of(settings.getTenantId(), settings.getId())); + cacheKeys.add(AiModelSettingsCacheKey.of(settings.getTenantId(), settings.getId())); }); - publishEvictEvent(new AiSettingsCacheEvictEvent(cacheKeys)); + publishEvictEvent(new AiModelSettingsCacheEvictEvent(cacheKeys)); } - private void publishDeleteEvent(AiSettings settings) { + private void publishDeleteEvent(AiModelSettings settings) { eventPublisher.publishEvent(DeleteEntityEvent.builder() .tenantId(settings.getTenantId()) .entityId(settings.getId()) @@ -170,7 +170,7 @@ class AiSettingsServiceImpl extends CachedVersionedEntityService skippedEntities = EnumSet.of( EntityType.ALARM, EntityType.QUEUE, EntityType.TB_RESOURCE, EntityType.OTA_PACKAGE, EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_TEMPLATE, - EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, EntityType.AI_SETTINGS + EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, EntityType.AI_MODEL_SETTINGS ); @TransactionalEventListener(fallbackExecution = true) // after transaction commit diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index ecd93df6fd..87c432268a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -740,15 +740,12 @@ public class ModelConstants { public static final String CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID = "calculated_field_id"; /** - * AI settings constants. - */ - public static final String AI_SETTINGS_TABLE_NAME = "ai_settings"; - public static final String AI_SETTINGS_TENANT_ID_COLUMN_NAME = TENANT_ID_COLUMN; - public static final String AI_SETTINGS_NAME_COLUMN_NAME = NAME_PROPERTY; - public static final String AI_SETTINGS_PROVIDER_COLUMN_NAME = "provider"; - public static final String AI_SETTINGS_PROVIDER_CONFIG_COLUMN_NAME = "provider_config"; - public static final String AI_SETTINGS_MODEL_COLUMN_NAME = "model"; - public static final String AI_SETTINGS_MODEL_CONFIG_COLUMN_NAME = "model_config"; + * AI model settings 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"; protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java similarity index 53% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java index e2775e3cbb..06774f5a7a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java @@ -18,23 +18,20 @@ package org.thingsboard.server.dao.model.sql; import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.Table; import lombok.Getter; import lombok.Setter; import lombok.ToString; import org.hibernate.annotations.Type; import org.hibernate.proxy.HibernateProxy; -import org.thingsboard.server.common.data.ai.AiSettings; -import org.thingsboard.server.common.data.ai.model.AiModelConfig; -import org.thingsboard.server.common.data.ai.provider.AiProvider; -import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.ai.model.AiModel; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseVersionedEntity; import org.thingsboard.server.dao.model.ModelConstants; +import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -42,53 +39,40 @@ import java.util.UUID; @Setter @ToString @Entity -@Table(name = ModelConstants.AI_SETTINGS_TABLE_NAME) -public class AiSettingsEntity extends BaseVersionedEntity { +@Table(name = ModelConstants.AI_MODEL_SETTINGS_TABLE_NAME) +public class AiModelSettingsEntity extends BaseVersionedEntity { - @Column(name = ModelConstants.AI_SETTINGS_TENANT_ID_COLUMN_NAME, nullable = false, columnDefinition = "UUID") + public static final Map COLUMN_MAP = Map.of( + "createdTime", "created_time" + ); + + @Column(name = ModelConstants.AI_MODEL_SETTINGS_TENANT_ID_COLUMN_NAME, nullable = false, columnDefinition = "UUID") private UUID tenantId; - @Column(name = ModelConstants.AI_SETTINGS_NAME_COLUMN_NAME, nullable = false) + @Column(name = ModelConstants.AI_MODEL_SETTINGS_NAME_COLUMN_NAME, nullable = false) private String name; - @Enumerated(EnumType.STRING) - @Column(name = ModelConstants.AI_SETTINGS_PROVIDER_COLUMN_NAME, nullable = false) - private AiProvider provider; - - @Type(JsonBinaryType.class) - @Column(name = ModelConstants.AI_SETTINGS_PROVIDER_CONFIG_COLUMN_NAME, nullable = false, columnDefinition = "JSONB") - private AiProviderConfig providerConfig; - - @Column(name = ModelConstants.AI_SETTINGS_MODEL_COLUMN_NAME, nullable = false) - private String model; - @Type(JsonBinaryType.class) - @Column(name = ModelConstants.AI_SETTINGS_MODEL_CONFIG_COLUMN_NAME, columnDefinition = "JSONB") - private AiModelConfig modelConfig; + @Column(name = ModelConstants.AI_MODEL_SETTINGS_CONFIGURATION_COLUMN_NAME, nullable = false, columnDefinition = "JSONB") + private AiModel configuration; - public AiSettingsEntity() {} + public AiModelSettingsEntity() {} - public AiSettingsEntity(AiSettings aiSettings) { - super(aiSettings); - tenantId = getTenantUuid(aiSettings.getTenantId()); - name = aiSettings.getName(); - provider = aiSettings.getProvider(); - providerConfig = aiSettings.getProviderConfig(); - model = aiSettings.getModel(); - modelConfig = aiSettings.getModelConfig(); + public AiModelSettingsEntity(AiModelSettings aiModelSettings) { + super(aiModelSettings); + tenantId = getTenantUuid(aiModelSettings.getTenantId()); + name = aiModelSettings.getName(); + configuration = aiModelSettings.getConfiguration(); } @Override - public AiSettings toData() { - var settings = new AiSettings(new AiSettingsId(id)); + public AiModelSettings toData() { + var settings = new AiModelSettings(new AiModelSettingsId(id)); settings.setCreatedTime(createdTime); settings.setVersion(version); settings.setTenantId(TenantId.fromUUID(tenantId)); settings.setName(name); - settings.setProvider(provider); - settings.setProviderConfig(providerConfig); - settings.setModel(model); - settings.setModelConfig(modelConfig); + settings.setConfiguration(configuration); return settings; } @@ -99,7 +83,7 @@ public class AiSettingsEntity extends BaseVersionedEntity { Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); if (thisEffectiveClass != oEffectiveClass) return false; - AiSettingsEntity that = (AiSettingsEntity) o; + AiModelSettingsEntity that = (AiModelSettingsEntity) o; return getId() != null && Objects.equals(getId(), that.getId()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java new file mode 100644 index 0000000000..6ed84e3c3e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java @@ -0,0 +1,86 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.ai.AiModelSettingsDao; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.tenant.TenantService; + +import java.util.Optional; + +@Component +@RequiredArgsConstructor +class AiModelSettingsDataValidator extends DataValidator { + + private final TenantService tenantService; + private final AiModelSettingsDao aiModelSettingsDao; + + @Override + protected void validateCreate(TenantId tenantId, AiModelSettings settings) { + validateNumberOfEntitiesPerTenant(tenantId, EntityType.AI_MODEL_SETTINGS); + } + + @Override + protected AiModelSettings validateUpdate(TenantId tenantId, AiModelSettings settings) { + Optional existing = aiModelSettingsDao.findByTenantIdAndId(tenantId, settings.getId()); + if (existing.isEmpty()) { + throw new DataValidationException("Cannot update non-existent AI model settings!"); + } + return existing.get(); + } + + @Override + protected void validateDataImpl(TenantId tenantId, AiModelSettings settings) { + // ID validation + if (settings.getId() != null) { + if (settings.getUuidId() == null) { + throw new DataValidationException("AI model settings UUID should be specified!"); + } + if (settings.getId().isNullUid()) { + throw new DataValidationException("AI model settings UUID must not be the reserved null value!"); + } + } + + // tenant ID validation + if (settings.getTenantId() == null || settings.getTenantId().getId() == null) { + throw new DataValidationException("AI model settings should be assigned to tenant!"); + } + if (settings.getTenantId().isSysTenantId()) { + throw new DataValidationException("AI model settings cannot be assigned to the system tenant!"); + } + if (!tenantService.tenantExists(tenantId)) { + throw new DataValidationException("AI model settings reference a non-existent tenant!"); + } + + // name validation + validateString("AI model settings name", settings.getName()); + if (settings.getName().length() > 255) { + throw new DataValidationException("AI model settings name should be between 1 and 255 symbols!"); + } + + // model config validation + if (settings.getConfiguration() == null) { + throw new DataValidationException("AI model settings configuration should be specified!"); + } + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiSettingsDataValidator.java deleted file mode 100644 index 24d4df23ee..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiSettingsDataValidator.java +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.validator; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ai.AiSettings; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.ai.AiSettingsDao; -import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.service.DataValidator; -import org.thingsboard.server.dao.tenant.TenantService; - -import java.util.Objects; -import java.util.Optional; - -@Component -@RequiredArgsConstructor -class AiSettingsDataValidator extends DataValidator { - - private final TenantService tenantService; - private final AiSettingsDao aiSettingsDao; - - @Override - protected void validateCreate(TenantId tenantId, AiSettings aiSettings) { - validateNumberOfEntitiesPerTenant(tenantId, EntityType.AI_SETTINGS); - } - - @Override - protected AiSettings validateUpdate(TenantId tenantId, AiSettings aiSettings) { - Optional old = aiSettingsDao.findByTenantIdAndId(tenantId, aiSettings.getId()); - if (old.isEmpty()) { - throw new DataValidationException("Cannot update non-existent AI settings!"); - } - return old.get(); - } - - @Override - protected void validateDataImpl(TenantId tenantId, AiSettings aiSettings) { - // ID validation - if (aiSettings.getId() != null) { - if (aiSettings.getUuidId() == null) { - throw new DataValidationException("AI settings UUID should be specified!"); - } - if (aiSettings.getId().isNullUid()) { - throw new DataValidationException("AI settings UUID must not be the reserved null value!"); - } - } - - // tenant ID validation - if (aiSettings.getTenantId() == null || aiSettings.getTenantId().getId() == null) { - throw new DataValidationException("AI settings should be assigned to tenant!"); - } - if (aiSettings.getTenantId().isSysTenantId()) { - throw new DataValidationException("AI settings cannot be assigned to the system tenant!"); - } - if (!tenantService.tenantExists(tenantId)) { - throw new DataValidationException("AI settings reference a non-existent tenant!"); - } - - // name validation - validateString("AI settings name", aiSettings.getName()); - if (aiSettings.getName().length() > 255) { - throw new DataValidationException("AI settings name should be between 1 and 255 symbols!"); - } - - // provider validation - if (aiSettings.getProvider() == null) { - throw new DataValidationException("AI provider should be specified!"); - } - - // provider config validation - if (aiSettings.getProviderConfig() == null) { - throw new DataValidationException("AI provider config should be specified!"); - } - if (aiSettings.getProviderConfig().getProvider() != aiSettings.getProvider()) { - throw new DataValidationException("AI provider configuration should match the selected AI provider!"); - } - validateString("AI provider API key", aiSettings.getProviderConfig().getApiKey()); - - // model identifier validation - validateString("AI model identifier", aiSettings.getModel()); - if (aiSettings.getModel().length() > 255) { - throw new DataValidationException("AI model identifier should be between 1 and 255 symbols!"); - } - - // model config validation - if (aiSettings.getModelConfig() != null) { - if (!Objects.equals(aiSettings.getModelConfig().getModel(), aiSettings.getModel())) { - throw new DataValidationException("AI model configuration should match the selected AI model!"); - } - } - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java similarity index 57% rename from dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java index a73569eac2..b645db8d66 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java @@ -22,30 +22,32 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; -import org.thingsboard.server.dao.model.sql.AiSettingsEntity; +import org.thingsboard.server.dao.model.sql.AiModelSettingsEntity; import java.util.Optional; import java.util.Set; import java.util.UUID; -public interface AiSettingsRepository extends JpaRepository { +interface AiModelSettingsRepository extends JpaRepository { - @Query("SELECT ai " + - "FROM AiSettingsEntity ai " + - "WHERE ai.tenantId = :tenantId " + - "AND (:textSearch IS NULL " + - "OR ilike(ai.name, CONCAT('%', :textSearch, '%')) = true " + - "OR ilike(ai.provider, CONCAT('%', :textSearch, '%')) = true " + - "OR ilike(ai.model, CONCAT('%', :textSearch, '%')) = true)") - Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); + @Query(nativeQuery = true, value = """ + SELECT * + FROM ai_model_settings ai_model + WHERE ai_model.tenant_id = :tenantId + AND (:textSearch IS NULL + OR ai_model.name ILIKE '%' || :textSearch || '%' + OR (ai_model.configuration -> 'providerConfig' ->> 'provider') ILIKE '%' || :textSearch || '%' + OR (ai_model.configuration ->> 'modelId') ILIKE '%' || :textSearch || '%') + """) + Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); - Optional findByTenantIdAndId(UUID tenantId, UUID id); + Optional findByTenantIdAndId(UUID tenantId, UUID id); long countByTenantId(UUID tenantId); @Transactional @Modifying - @Query("DELETE FROM AiSettingsEntity ai WHERE ai.id IN (:ids)") + @Query("DELETE FROM AiModelSettingsEntity ai_model WHERE ai_model.id IN (:ids)") int deleteByIdIn(@Param("ids") Set ids); @Transactional @@ -53,7 +55,7 @@ public interface AiSettingsRepository extends JpaRepository ids); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java index c56c29b835..e5d7df2ee9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java @@ -20,14 +20,14 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ai.AiSettings; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.ai.AiSettingsDao; -import org.thingsboard.server.dao.model.sql.AiSettingsEntity; +import org.thingsboard.server.dao.ai.AiModelSettingsDao; +import org.thingsboard.server.dao.model.sql.AiModelSettingsEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; @@ -38,55 +38,55 @@ import java.util.UUID; @SqlDao @Component @RequiredArgsConstructor -class JpaAiSettingsDao extends JpaAbstractDao implements AiSettingsDao { +class JpaAiSettingsDao extends JpaAbstractDao implements AiModelSettingsDao { - private final AiSettingsRepository aiSettingsRepository; + private final AiModelSettingsRepository aiModelSettingsRepository; @Override - public Optional findByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { - return aiSettingsRepository.findByTenantIdAndId(tenantId.getId(), aiSettingsId.getId()).map(DaoUtil::getData); + public Optional findByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) { + return aiModelSettingsRepository.findByTenantIdAndId(tenantId.getId(), settingsId.getId()).map(DaoUtil::getData); } @Override - public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { - return DaoUtil.toPageData(aiSettingsRepository.findByTenantId( - tenantId.getId(), StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), DaoUtil.toPageable(pageLink)) + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(aiModelSettingsRepository.findByTenantId( + tenantId.getId(), StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), DaoUtil.toPageable(pageLink, AiModelSettingsEntity.COLUMN_MAP)) ); } @Override public Long countByTenantId(TenantId tenantId) { - return aiSettingsRepository.countByTenantId(tenantId.getId()); + return aiModelSettingsRepository.countByTenantId(tenantId.getId()); } @Override - public boolean deleteById(TenantId tenantId, AiSettingsId aiSettingsId) { - return aiSettingsRepository.deleteByIdIn(Set.of(aiSettingsId.getId())) > 0; + public boolean deleteById(TenantId tenantId, AiModelSettingsId settingsId) { + return aiModelSettingsRepository.deleteByIdIn(Set.of(settingsId.getId())) > 0; } @Override public int deleteByTenantId(TenantId tenantId) { - return aiSettingsRepository.deleteByTenantId(tenantId.getId()); + return aiModelSettingsRepository.deleteByTenantId(tenantId.getId()); } @Override - public boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { - return aiSettingsRepository.deleteByTenantIdAndIdIn(tenantId.getId(), Set.of(aiSettingsId.getId())) > 0; + public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) { + return aiModelSettingsRepository.deleteByTenantIdAndIdIn(tenantId.getId(), Set.of(settingsId.getId())) > 0; } @Override public EntityType getEntityType() { - return EntityType.AI_SETTINGS; + return EntityType.AI_MODEL_SETTINGS; } @Override - protected Class getEntityClass() { - return AiSettingsEntity.class; + protected Class getEntityClass() { + return AiModelSettingsEntity.class; } @Override - protected JpaRepository getRepository() { - return aiSettingsRepository; + protected JpaRepository getRepository() { + return aiModelSettingsRepository; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 505fe2a50a..ef85ba30c3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -183,7 +183,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService> ChatModel configureChatModel(AiChatModel chatModel); } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index e5306c0193..ab0f3bd050 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -44,7 +44,7 @@ import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.ai.AiSettingsService; +import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -418,9 +418,9 @@ public interface TbContext { AuditLogService getAuditLogService(); - RuleEngineAiService getAiService(); + RuleEngineAiModelService getAiModelService(); - AiSettingsService getAiSettingsService(); + AiModelSettingsService getAiModelSettingsService(); AiRequestsExecutor getAiRequestsExecutor(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 4d56b43c8e..2ac3ea3e6c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -36,9 +36,11 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.external.TbAbstractExternalNode; -import org.thingsboard.server.common.data.ai.model.AiModelConfig; -import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.ai.model.AiModelType; +import org.thingsboard.server.common.data.ai.model.chat.AiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsg; @@ -46,6 +48,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; import java.util.List; import java.util.NoSuchElementException; +import java.util.Optional; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields; @@ -64,7 +67,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { private String userPrompt; private ResponseFormat responseFormat; private int timeoutSeconds; - private AiSettingsId aiSettingsId; + private AiModelSettingsId modelSettingsId; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { @@ -86,11 +89,16 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { systemPrompt = config.getSystemPrompt(); userPrompt = config.getUserPrompt(); timeoutSeconds = config.getTimeoutSeconds(); + modelSettingsId = config.getAiModelSettingsId(); - if (!aiSettingsExist(ctx, config.getAiSettingsId())) { - throw new TbNodeException("[" + ctx.getTenantId() + "] AI settings with ID: " + config.getAiSettingsId() + " were not found", true); + Optional modelSettings = ctx.getAiModelSettingsService().findAiModelSettingsByTenantIdAndId(ctx.getTenantId(), modelSettingsId); + if (modelSettings.isEmpty()) { + throw new TbNodeException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] were not found", true); + } + AiModelType modelType = modelSettings.get().getConfiguration().modelType(); + if (modelType != AiModelType.CHAT) { + throw new TbNodeException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] must be of type CHAT, but was " + modelType, true); } - aiSettingsId = config.getAiSettingsId(); } private static JsonSchema getJsonSchema(ResponseFormatType responseFormatType, ObjectNode jsonSchema) { @@ -100,12 +108,8 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { return responseFormatType == ResponseFormatType.JSON && jsonSchema != null ? Langchain4jJsonSchemaAdapter.fromJsonNode(jsonSchema) : null; } - private static boolean aiSettingsExist(TbContext ctx, AiSettingsId aiSettingsId) { - return ctx.getAiSettingsService().findAiSettingsByTenantIdAndId(ctx.getTenantId(), aiSettingsId).isPresent(); - } - @Override - public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { + public void onMsg(TbContext ctx, TbMsg msg) { var ackedMsg = ackIfNeeded(ctx, msg); var systemMessage = SystemMessage.from(TbNodeUtils.processPattern(systemPrompt, ackedMsg)); @@ -137,19 +141,25 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { }, directExecutor()); } - private FluentFuture configureChatModelAsync(TbContext ctx) { - return ctx.getAiSettingsService().findAiSettingsByTenantIdAndIdAsync(ctx.getTenantId(), aiSettingsId).transform(aiSettingsOpt -> { - if (aiSettingsOpt.isEmpty()) { - throw new NoSuchElementException("AI settings with ID: " + aiSettingsId + " were not found"); + private > FluentFuture configureChatModelAsync(TbContext ctx) { + return ctx.getAiModelSettingsService().findAiModelSettingsByTenantIdAndIdAsync(ctx.getTenantId(), modelSettingsId).transform(settingsOpt -> { + if (settingsOpt.isEmpty()) { + throw new NoSuchElementException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] were not found"); + } + AiModelSettings settings = settingsOpt.get(); + AiModelType modelType = settings.getConfiguration().modelType(); + if (modelType != AiModelType.CHAT) { + throw new IllegalStateException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] must be of type CHAT, but was " + modelType); } - AiProviderConfig providerConfig = aiSettingsOpt.get().getProviderConfig(); - AiModelConfig modelConfig = aiSettingsOpt.get().getModelConfig(); + @SuppressWarnings("unchecked") + AiChatModel chatModel = (AiChatModel) settingsOpt.get().getConfiguration(); - modelConfig.setTimeoutSeconds(timeoutSeconds); - modelConfig.setMaxRetries(0); // disable retries to respect timeout set in rule node config + chatModel = chatModel.withModelConfig(chatModel.modelConfig() + .withTimeoutSeconds(timeoutSeconds) + .withMaxRetries(0)); // disable retries to respect timeout set in rule node config - return ctx.getAiService().configureChatModel(providerConfig, modelConfig); + return ctx.getAiModelService().configureChatModel(chatModel); }, ctx.getDbCallbackExecutor()); } @@ -172,7 +182,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { systemPrompt = null; userPrompt = null; responseFormat = null; - aiSettingsId = null; + modelSettingsId = null; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index 96ecde330a..4c99b6f613 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -26,14 +26,14 @@ import jakarta.validation.constraints.NotNull; import lombok.Data; import org.thingsboard.common.util.JsonSchemaUtils; import org.thingsboard.rule.engine.api.NodeConfiguration; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.validation.Length; @Data public class TbAiNodeConfiguration implements NodeConfiguration { @NotNull - private AiSettingsId aiSettingsId; + private AiModelSettingsId aiModelSettingsId; @NotBlank @Length(min = 1, max = 1000) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java index 62ca65db1b..0df2652ccb 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -19,7 +19,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.AssetId; @@ -176,8 +176,8 @@ public class TenantIdLoader { tenantEntity = null; } break; - case AI_SETTINGS: - tenantEntity = ctx.getAiSettingsService().findAiSettingsById(ctxTenantId, new AiSettingsId(id)).orElse(null); + case AI_MODEL_SETTINGS: + tenantEntity = ctx.getAiModelSettingsService().findAiModelSettingsById(ctxTenantId, new AiModelSettingsId(id)).orElse(null); break; default: throw new RuntimeException("Unexpected entity type: " + entityId.getEntityType()); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index c763a1b069..873393ef2b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -40,7 +40,7 @@ import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; @@ -69,7 +69,7 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.dao.ai.AiSettingsService; +import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; @@ -164,7 +164,7 @@ public class TenantIdLoaderTest { @Mock private CalculatedFieldService calculatedFieldService; @Mock - private AiSettingsService aiSettingsService; + private AiModelSettingsService aiModelSettingsService; private TenantId tenantId; private TenantProfileId tenantProfileId; @@ -424,11 +424,11 @@ public class TenantIdLoaderTest { when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService); doReturn(calculatedFieldLink).when(calculatedFieldService).findCalculatedFieldLinkById(eq(tenantId), any()); break; - case AI_SETTINGS: - AiSettings aiSettings = new AiSettings(); - aiSettings.setTenantId(tenantId); - when(ctx.getAiSettingsService()).thenReturn(aiSettingsService); - doReturn(Optional.of(aiSettings)).when(aiSettingsService).findAiSettingsById(eq(tenantId), any()); + case AI_MODEL_SETTINGS: + AiModelSettings aiModelSettings = new AiModelSettings(); + aiModelSettings.setTenantId(tenantId); + when(ctx.getAiModelSettingsService()).thenReturn(aiModelSettingsService); + doReturn(Optional.of(aiModelSettings)).when(aiModelSettingsService).findAiModelSettingsById(eq(tenantId), any()); break; default: throw new RuntimeException("Unexpected originator EntityType " + entityType); From bfee0c9b4699840b65d6f51a27e406c47f183f97 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 23 Jun 2025 16:17:17 +0300 Subject: [PATCH 079/435] AI rule node: correct texts to reflect renaming --- .../thingsboard/server/controller/ControllerConstants.java | 2 +- ui-ngx/src/app/shared/models/entity-type.models.ts | 4 ++-- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index b27c08b52c..a46455a47b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/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_SETTINGS_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the AI settings name, provider and model."; + protected static final String AI_SETTINGS_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the AI model settings 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."; diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 0c7b6ca152..3f70ad4b7c 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -496,8 +496,8 @@ export const entityTypeTranslations = new Map Date: Mon, 23 Jun 2025 17:28:16 +0300 Subject: [PATCH 080/435] UI: Clear code and omit label --- .../home/pages/ota-update/ota-update-tabs.component.ts | 4 ---- ui-ngx/src/app/shared/models/ota-package.models.ts | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts index 1de3031ccb..44d17f3c11 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts @@ -37,8 +37,4 @@ export class OtaUpdateTabsComponent extends EntityTabsComponent { return this.entity && this.entity.tenantId.id !== NULL_UUID; } - ngOnInit() { - super.ngOnInit(); - } - } diff --git a/ui-ngx/src/app/shared/models/ota-package.models.ts b/ui-ngx/src/app/shared/models/ota-package.models.ts index 234886063e..bba7ea3d04 100644 --- a/ui-ngx/src/app/shared/models/ota-package.models.ts +++ b/ui-ngx/src/app/shared/models/ota-package.models.ts @@ -86,7 +86,7 @@ export interface OtaPagesIds { softwareId?: OtaPackageId; } -export interface OtaPackageInfo extends BaseData, HasTenantId, ExportableEntity { +export interface OtaPackageInfo extends Omit, 'label'>, HasTenantId, ExportableEntity { tenantId?: TenantId; type: OtaUpdateType; deviceProfileId?: DeviceProfileId; From 5949fa1ac81a24a6c1acdf2db0a1f3e7c2975f3e Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 23 Jun 2025 18:01:26 +0300 Subject: [PATCH 081/435] Small refactoring of OtaPackageController --- .../controller/OtaPackageController.java | 35 +++++++------------ .../ota/DefaultTbOtaPackageService.java | 1 + 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java index 43ebf89b41..d4f932a643 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java @@ -24,13 +24,14 @@ import org.springframework.core.io.ByteArrayResource; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +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.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.thingsboard.server.common.data.OtaPackage; @@ -49,8 +50,6 @@ import org.thingsboard.server.service.entitiy.ota.TbOtaPackageService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; -import java.io.IOException; - import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.OTA_PACKAGE_DESCRIPTION; @@ -80,8 +79,7 @@ public class OtaPackageController extends BaseController { @ApiOperation(value = "Download OTA Package (downloadOtaPackage)", notes = "Download OTA Package based on the provided OTA Package Id." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority( 'TENANT_ADMIN')") - @RequestMapping(value = "/otaPackage/{otaPackageId}/download", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/otaPackage/{otaPackageId}/download") public ResponseEntity downloadOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); @@ -105,8 +103,7 @@ public class OtaPackageController extends BaseController { notes = "Fetch the OTA Package Info object based on the provided OTA Package Id. " + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/otaPackage/info/{otaPackageId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/otaPackage/info/{otaPackageId}") public OtaPackageInfo getOtaPackageInfoById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); @@ -118,8 +115,7 @@ public class OtaPackageController extends BaseController { notes = "Fetch the OTA Package object based on the provided OTA Package Id. " + "The server checks that the OTA Package is owned by the same tenant. " + OTA_PACKAGE_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/otaPackage/{otaPackageId}") public OtaPackage getOtaPackageById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); @@ -134,10 +130,9 @@ public class OtaPackageController extends BaseController { "Referencing non-existing OTA Package Id will cause 'Not Found' error. " + "\n\nOTA Package combination of the title with the version is unique in the scope of tenant. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/otaPackage", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/otaPackage") public OtaPackageInfo saveOtaPackageInfo(@Parameter(description = "A JSON value representing the OTA Package.") - @RequestBody SaveOtaPackageInfoRequest otaPackageInfo) throws ThingsboardException { + @RequestBody SaveOtaPackageInfoRequest otaPackageInfo) throws Exception { otaPackageInfo.setTenantId(getTenantId()); checkEntity(otaPackageInfo.getId(), otaPackageInfo, Resource.OTA_PACKAGE); @@ -148,8 +143,7 @@ public class OtaPackageController extends BaseController { notes = "Update the OTA Package. Adds the date to the existing OTA Package Info" + TENANT_AUTHORITY_PARAGRAPH, requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(mediaType = MULTIPART_FORM_DATA_VALUE))) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.POST, consumes = MULTIPART_FORM_DATA_VALUE) - @ResponseBody + @PostMapping(value = "/otaPackage/{otaPackageId}", consumes = MULTIPART_FORM_DATA_VALUE) public OtaPackageInfo saveOtaPackageData(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId, @Parameter(description = "OTA Package checksum. For example, '0xd87f7e0c'") @@ -157,7 +151,7 @@ public class OtaPackageController extends BaseController { @Parameter(description = "OTA Package checksum algorithm.", schema = @Schema(allowableValues = {"MD5", "SHA256", "SHA384", "SHA512", "CRC32", "MURMUR3_32", "MURMUR3_128"})) @RequestParam(CHECKSUM_ALGORITHM) String checksumAlgorithmStr, @Parameter(description = "OTA Package data.") - @RequestPart MultipartFile file) throws ThingsboardException, IOException { + @RequestPart MultipartFile file) throws Exception { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); checkParameter(CHECKSUM_ALGORITHM, checksumAlgorithmStr); OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); @@ -172,8 +166,7 @@ public class OtaPackageController extends BaseController { notes = "Returns a page of OTA Package Info objects owned by tenant. " + PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/otaPackages", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/otaPackages") public PageData getOtaPackages(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @@ -192,8 +185,7 @@ public class OtaPackageController extends BaseController { notes = "Returns a page of OTA Package Info objects owned by tenant. " + PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/otaPackages/{deviceProfileId}/{type}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/otaPackages/{deviceProfileId}/{type}") public PageData getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @PathVariable("deviceProfileId") String strDeviceProfileId, @Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"})) @@ -219,8 +211,7 @@ public class OtaPackageController extends BaseController { notes = "Deletes the OTA Package. Referencing non-existing OTA Package Id will cause an error. " + "Can't delete the OTA Package if it is referenced by existing devices or device profile." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.DELETE) - @ResponseBody + @DeleteMapping(value = "/otaPackage/{otaPackageId}") public void deleteOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable("otaPackageId") String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java index af8bbeb669..2d597f7053 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java @@ -110,4 +110,5 @@ public class DefaultTbOtaPackageService extends AbstractTbEntityService implemen throw e; } } + } From c653a2b695aee491bb7950d68c76c54b418e83a6 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 23 Jun 2025 20:06:39 +0300 Subject: [PATCH 082/435] AI rule node: add version control support --- .../main/data/upgrade/basic/schema_update.sql | 4 +- .../controller/AiModelSettingsController.java | 4 +- .../controller/ControllerConstants.java | 2 +- .../entitiy/AbstractTbEntityService.java | 4 +- .../ai/DefaultTbAiModelSettingsService.java | 1 + .../DefaultEntitiesExportImportService.java | 3 +- .../impl/AiModelSettingsExportService.java | 36 +++++++++ .../impl/AiModelSettingsImportService.java | 77 +++++++++++++++++++ .../DefaultEntitiesVersionControlService.java | 7 +- .../vc/EntitiesVersionControlService.java | 4 +- .../common/data/ai/AiModelSettings.java | 26 +++++-- .../server/common/data/sync/JsonTbEntity.java | 7 +- .../server/dao/ai/AiModelSettingsDao.java | 4 +- .../dao/ai/AiModelSettingsServiceImpl.java | 4 +- .../dao/model/sql/AiModelSettingsEntity.java | 5 ++ .../dao/sql/ai/AiModelSettingsRepository.java | 13 +++- ...ngsDao.java => JpaAiModelSettingsDao.java} | 31 +++++++- .../main/resources/sql/schema-entities.sql | 4 +- .../app/shared/models/entity-type.models.ts | 4 +- 19 files changed, 207 insertions(+), 33 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelSettingsExportService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelSettingsImportService.java rename dao/src/main/java/org/thingsboard/server/dao/sql/ai/{JpaAiSettingsDao.java => JpaAiModelSettingsDao.java} (69%) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index f77c8dce67..3aa5a03b11 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -16,10 +16,12 @@ CREATE TABLE ai_model_settings ( id UUID NOT NULL PRIMARY KEY, + external_id UUID, created_time BIGINT NOT NULL, tenant_id UUID NOT NULL, 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_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT ai_model_settings_external_id_unq_key UNIQUE (tenant_id, external_id) ); diff --git a/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java index 891f6b176c..69c3181e26 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java @@ -38,7 +38,7 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; -import static org.thingsboard.server.controller.ControllerConstants.AI_SETTINGS_TEXT_SEARCH_DESCRIPTION; +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; @@ -99,7 +99,7 @@ public class AiModelSettingsController extends BaseController { @RequestParam int pageSize, @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, - @Parameter(description = AI_SETTINGS_TEXT_SEARCH_DESCRIPTION) + @Parameter(description = AI_MODEL_SETTINGS_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty, diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index a46455a47b..20e350f0b8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/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_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_SETTINGS_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the AI model settings 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."; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index 476a4ef5ca..81fd1478b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -97,7 +97,7 @@ public abstract class AbstractTbEntityService { return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); } - protected ListenableFuture autoCommit(User user, EntityId entityId) throws Exception { + protected ListenableFuture autoCommit(User user, EntityId entityId) { if (vcService != null) { return vcService.autoCommit(user, entityId); } else { @@ -106,7 +106,7 @@ public abstract class AbstractTbEntityService { } } - protected ListenableFuture autoCommit(User user, EntityType entityType, List entityIds) throws Exception { + protected ListenableFuture autoCommit(User user, EntityType entityType, List entityIds) { if (vcService != null) { return vcService.autoCommit(user, entityType, entityIds); } else { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java index 5a41d83eed..69f130461c 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java @@ -44,6 +44,7 @@ class DefaultTbAiModelSettingsService extends AbstractTbEntityService implements AiModelSettings savedSettings; try { savedSettings = aiModelSettingsService.save(settings); + autoCommit(user, savedSettings.getId()); } catch (Exception e) { logEntityActionService.logEntityAction(tenantId, requireNonNullElseGet(settings.getId(), () -> emptyId(EntityType.AI_MODEL_SETTINGS)), settings, actionType, user, e); throw e; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index db7e37b368..82f855b7c2 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -69,7 +69,8 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS EntityType.DASHBOARD, EntityType.ASSET_PROFILE, EntityType.ASSET, EntityType.DEVICE_PROFILE, EntityType.DEVICE, EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE, - EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE + EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, + EntityType.AI_MODEL_SETTINGS ); @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelSettingsExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelSettingsExportService.java new file mode 100644 index 0000000000..a5ba16e9b1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelSettingsExportService.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.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.sync.ie.EntityExportData; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.Set; + +@Service +@TbCoreComponent +class AiModelSettingsExportService extends BaseEntityExportService> { + + @Override + public Set getSupportedEntityTypes() { + return Set.of(EntityType.AI_MODEL_SETTINGS); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelSettingsImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelSettingsImportService.java new file mode 100644 index 0000000000..e5666e5003 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelSettingsImportService.java @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.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.id.TenantId; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.dao.ai.AiModelSettingsService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; + +@Service +@TbCoreComponent +@RequiredArgsConstructor +class AiModelSettingsImportService extends BaseEntityImportService> { + + private final AiModelSettingsService aiModelSettingsService; + + @Override + protected void setOwner( + TenantId tenantId, + AiModelSettings settings, + BaseEntityImportService>.IdProvider idProvider + ) { + settings.setTenantId(tenantId); + } + + @Override + protected AiModelSettings prepare( + EntitiesImportCtx ctx, + AiModelSettings settings, + AiModelSettings oldEntity, + EntityExportData exportData, + BaseEntityImportService>.IdProvider idProvider + ) { + return settings; + } + + @Override + protected AiModelSettings deepCopy(AiModelSettings settings) { + return new AiModelSettings(settings); + } + + @Override + protected AiModelSettings saveOrUpdate( + EntitiesImportCtx ctx, + AiModelSettings settings, + EntityExportData exportData, + BaseEntityImportService>.IdProvider idProvider, + CompareResult compareResult + ) { + return aiModelSettingsService.save(settings); + } + + @Override + public EntityType getEntityType() { + return EntityType.AI_MODEL_SETTINGS; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java index 420e85dc6c..5fe891c847 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java @@ -114,9 +114,8 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont private final TbTransactionalCache taskCache; private final VersionControlExecutor executor; - @SuppressWarnings("UnstableApiUsage") @Override - public ListenableFuture saveEntitiesVersion(User user, VersionCreateRequest request) throws Exception { + public ListenableFuture saveEntitiesVersion(User user, VersionCreateRequest request) { checkBranchName(request.getBranch()); var pendingCommit = gitServiceQueue.prepareCommit(user, request); DonAsynchron.withCallback(pendingCommit, commit -> { @@ -546,7 +545,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont } @Override - public ListenableFuture autoCommit(User user, EntityId entityId) throws Exception { + public ListenableFuture autoCommit(User user, EntityId entityId) { var repositorySettings = repositorySettingsService.get(user.getTenantId()); if (repositorySettings == null || repositorySettings.isReadOnly()) { return Futures.immediateFuture(null); @@ -573,7 +572,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont } @Override - public ListenableFuture autoCommit(User user, EntityType entityType, List entityIds) throws Exception { + public ListenableFuture autoCommit(User user, EntityType entityType, List entityIds) { var repositorySettings = repositorySettingsService.get(user.getTenantId()); if (repositorySettings == null || repositorySettings.isReadOnly()) { return Futures.immediateFuture(null); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java index 3f22120d9c..e5ff89ebbd 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java @@ -69,9 +69,9 @@ public interface EntitiesVersionControlService { ListenableFuture checkVersionControlAccess(TenantId tenantId, RepositorySettings settings) throws Exception; - ListenableFuture autoCommit(User user, EntityId entityId) throws Exception; + ListenableFuture autoCommit(User user, EntityId entityId); - ListenableFuture autoCommit(User user, EntityType entityType, List entityIds) throws Exception; + ListenableFuture autoCommit(User user, EntityType entityType, List entityIds); ListenableFuture getEntityDataInfo(User user, EntityId entityId, String versionId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java index df1dbe4d7e..750f779e68 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java @@ -21,7 +21,7 @@ import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.BaseData; -import org.thingsboard.server.common.data.HasName; +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; @@ -34,7 +34,7 @@ import java.io.Serial; @Builder @AllArgsConstructor @EqualsAndHashCode(callSuper = true) -public final class AiModelSettings extends BaseData implements HasTenantId, HasVersion, HasName { +public final class AiModelSettings extends BaseData implements HasTenantId, HasVersion, ExportableEntity { @Serial private static final long serialVersionUID = 9017108678716011604L; @@ -45,7 +45,7 @@ public final class AiModelSettings extends BaseData implement description = "JSON object representing the ID of the tenant associated with these AI model settings", example = "e3c4b7d2-5678-4a9b-0c1d-2e3f4a5b6c7d" ) - TenantId tenantId; + private TenantId tenantId; @Schema( requiredMode = Schema.RequiredMode.REQUIRED, @@ -54,22 +54,24 @@ public final class AiModelSettings extends BaseData implement example = "7", defaultValue = "1" ) - Long version; + private Long version; @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 = "Default AI Settings" + example = "Rule node assistant" ) - String name; + private String name; @Schema( requiredMode = Schema.RequiredMode.NOT_REQUIRED, accessMode = Schema.AccessMode.READ_WRITE, description = "Configuration of the AI model" ) - AiModel configuration; + private AiModel configuration; + + private AiModelSettingsId externalId; public AiModelSettings() {} @@ -77,4 +79,14 @@ public final class AiModelSettings extends BaseData implement 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()); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java index c763daeb7f..6914b75636 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java @@ -26,6 +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.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.notification.rule.NotificationRule; @@ -58,8 +59,8 @@ import java.lang.annotation.Target; @Type(name = "NOTIFICATION_TEMPLATE", value = NotificationTemplate.class), @Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class), @Type(name = "NOTIFICATION_RULE", value = NotificationRule.class), - @Type(name = "TB_RESOURCE", value = TbResource.class) + @Type(name = "TB_RESOURCE", value = TbResource.class), + @Type(name = "AI_MODEL_SETTINGS", value = AiModelSettings.class) }) @JsonIgnoreProperties(value = {"tenantId", "createdTime", "version"}, ignoreUnknown = true) -public @interface JsonTbEntity { -} +public @interface JsonTbEntity {} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java index ae47af0db2..5ee0d49121 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java @@ -18,12 +18,12 @@ 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.id.TenantId; -import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.TenantEntityDao; import java.util.Optional; -public interface AiModelSettingsDao extends Dao, TenantEntityDao { +public interface AiModelSettingsDao extends TenantEntityDao, ExportableEntityDao { Optional findByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java index 2fa47bf60f..3875169305 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java @@ -65,7 +65,9 @@ class AiModelSettingsServiceImpl extends CachedVersionedEntityService @Column(name = ModelConstants.AI_MODEL_SETTINGS_CONFIGURATION_COLUMN_NAME, nullable = false, columnDefinition = "JSONB") private AiModel configuration; + @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY, columnDefinition = "UUID") + private UUID externalId; + public AiModelSettingsEntity() {} public AiModelSettingsEntity(AiModelSettings aiModelSettings) { @@ -63,6 +66,7 @@ public class AiModelSettingsEntity extends BaseVersionedEntity tenantId = getTenantUuid(aiModelSettings.getTenantId()); name = aiModelSettings.getName(); configuration = aiModelSettings.getConfiguration(); + externalId = getUuid(aiModelSettings.getExternalId()); } @Override @@ -73,6 +77,7 @@ public class AiModelSettingsEntity extends BaseVersionedEntity settings.setTenantId(TenantId.fromUUID(tenantId)); settings.setName(name); settings.setConfiguration(configuration); + settings.setExternalId(getEntityId(externalId, AiModelSettingsId::new)); return settings; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java index b645db8d66..e5909c5818 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java @@ -22,13 +22,18 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.AiModelSettingsEntity; import java.util.Optional; import java.util.Set; import java.util.UUID; -interface AiModelSettingsRepository extends JpaRepository { +interface AiModelSettingsRepository extends JpaRepository, ExportableEntityRepository { + + Optional findByTenantIdAndId(UUID tenantId, UUID id); + + Optional findByTenantIdAndName(UUID tenantId, String name); @Query(nativeQuery = true, value = """ SELECT * @@ -41,7 +46,11 @@ interface AiModelSettingsRepository extends JpaRepository findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); - Optional findByTenantIdAndId(UUID tenantId, UUID id); + @Query("SELECT ai_model.id FROM AiModelSettingsEntity ai_model WHERE ai_model.tenantId = :tenantId") + Page findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + + @Query("SELECT externalId FROM AiModelSettingsEntity WHERE id = :id") + Optional getExternalIdById(@Param("id") UUID id); long countByTenantId(UUID tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java similarity index 69% rename from dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java index e5d7df2ee9..e7b87b0a1a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java @@ -38,7 +38,7 @@ import java.util.UUID; @SqlDao @Component @RequiredArgsConstructor -class JpaAiSettingsDao extends JpaAbstractDao implements AiModelSettingsDao { +class JpaAiModelSettingsDao extends JpaAbstractDao implements AiModelSettingsDao { private final AiModelSettingsRepository aiModelSettingsRepository; @@ -47,13 +47,40 @@ class JpaAiSettingsDao extends JpaAbstractDao findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findByTenantId(UUID tenantId, PageLink pageLink) { return DaoUtil.toPageData(aiModelSettingsRepository.findByTenantId( - tenantId.getId(), StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), DaoUtil.toPageable(pageLink, AiModelSettingsEntity.COLUMN_MAP)) + tenantId, StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), DaoUtil.toPageable(pageLink, AiModelSettingsEntity.COLUMN_MAP)) + ); + } + + @Override + public PageData findIdsByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.pageToPageData( + aiModelSettingsRepository.findIdsByTenantId(tenantId, DaoUtil.toPageable(pageLink, AiModelSettingsEntity.COLUMN_MAP)).map(AiModelSettingsId::new) ); } + @Override + public AiModelSettingsId getExternalIdByInternal(AiModelSettingsId internalId) { + return aiModelSettingsRepository.getExternalIdById(internalId.getId()).map(AiModelSettingsId::new).orElse(null); + } + @Override public Long countByTenantId(TenantId tenantId) { return aiModelSettingsRepository.countByTenantId(tenantId.getId()); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index cf2b64175e..82d3aea66b 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -951,10 +951,12 @@ CREATE TABLE IF NOT EXISTS cf_debug_event ( CREATE TABLE IF NOT EXISTS ai_model_settings ( id UUID NOT NULL PRIMARY KEY, + external_id UUID, created_time BIGINT NOT NULL, tenant_id UUID NOT NULL, 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_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT ai_model_settings_external_id_unq_key UNIQUE (tenant_id, external_id) ); diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 3f70ad4b7c..02bae1576f 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -51,7 +51,7 @@ export enum EntityType { MOBILE_APP_BUNDLE = 'MOBILE_APP_BUNDLE', MOBILE_APP = 'MOBILE_APP', CALCULATED_FIELD = 'CALCULATED_FIELD', - AI_SETTINGS = 'AI_SETTINGS', + AI_MODEL_SETTINGS = 'AI_MODEL_SETTINGS', } export enum AliasEntityType { @@ -494,7 +494,7 @@ export const entityTypeTranslations = new Map Date: Mon, 23 Jun 2025 21:09:40 +0300 Subject: [PATCH 083/435] AI rule node: add basic support for Azure OpenAI --- application/pom.xml | 4 ++ .../Langchain4jChatModelConfigurerImpl.java | 13 ++++ .../data/ai/model/chat/AiChatModel.java | 2 +- .../data/ai/model/chat/AiChatModelConfig.java | 2 +- .../ai/model/chat/AzureOpenAiChatModel.java | 60 +++++++++++++++++++ .../chat/Langchain4jChatModelConfigurer.java | 2 + .../common/data/ai/provider/AiProvider.java | 1 + .../data/ai/provider/AiProviderConfig.java | 3 +- .../provider/AzureOpenAiProviderConfig.java | 30 ++++++++++ 9 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java diff --git a/application/pom.xml b/application/pom.xml index 47459950c7..2c11c50d3d 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -381,6 +381,10 @@ org.rocksdb rocksdbjni + + dev.langchain4j + langchain4j-azure-open-ai + dev.langchain4j langchain4j-open-ai diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index f66f31400b..d3b23b65b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.ai; import dev.langchain4j.model.chat.ChatModel; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModel; import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModel; import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModel; @@ -39,6 +40,18 @@ public class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelC .build(); } + @Override + public ChatModel configureChatModel(AzureOpenAiChatModel chatModel) { + AzureOpenAiChatModel.Config modelConfig = chatModel.modelConfig(); + return dev.langchain4j.model.azure.AzureOpenAiChatModel.builder() + .apiKey(chatModel.providerConfig().apiKey()) + .deploymentName(chatModel.modelId()) + .temperature(modelConfig.temperature()) + .timeout(toDuration(modelConfig.timeoutSeconds())) + .maxRetries(modelConfig.maxRetries()) + .build(); + } + @Override public ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel) { GoogleAiGeminiChatModel.Config modelConfig = chatModel.modelConfig(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java index 17f79d93e2..4e944ed6ec 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java @@ -20,7 +20,7 @@ import org.thingsboard.server.common.data.ai.model.AiModel; import org.thingsboard.server.common.data.ai.model.AiModelType; public sealed interface AiChatModel> extends AiModel - permits OpenAiChatModel, GoogleAiGeminiChatModel, MistralAiChatModel { + permits OpenAiChatModel, AzureOpenAiChatModel, GoogleAiGeminiChatModel, MistralAiChatModel { ChatModel configure(Langchain4jChatModelConfigurer configurer); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java index 565923cf6c..e44c9c5fa1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java @@ -18,7 +18,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import org.thingsboard.server.common.data.ai.model.AiModelConfig; public sealed interface AiChatModelConfig> extends AiModelConfig - permits OpenAiChatModel.Config, GoogleAiGeminiChatModel.Config, MistralAiChatModel.Config { + permits OpenAiChatModel.Config, AzureOpenAiChatModel.Config, GoogleAiGeminiChatModel.Config, MistralAiChatModel.Config { Double temperature(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java new file mode 100644 index 0000000000..37a1d84095 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; + +public record AzureOpenAiChatModel( + AzureOpenAiProviderConfig providerConfig, + String modelId, + Config modelConfig +) implements AiChatModel { + + public record Config( + Double temperature, + Integer timeoutSeconds, + Integer maxRetries + ) implements AiChatModelConfig { + + @Override + public AzureOpenAiChatModel.Config withTemperature(Double temperature) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + @Override + public AzureOpenAiChatModel.Config withTimeoutSeconds(Integer timeoutSeconds) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + @Override + public AzureOpenAiChatModel.Config withMaxRetries(Integer maxRetries) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public AzureOpenAiChatModel withModelConfig(AzureOpenAiChatModel.Config config) { + return new AzureOpenAiChatModel(providerConfig, modelId, config); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java index 87d317eb0b..41dd1fe4ad 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java @@ -21,6 +21,8 @@ public interface Langchain4jChatModelConfigurer { ChatModel configureChatModel(OpenAiChatModel chatModel); + ChatModel configureChatModel(AzureOpenAiChatModel chatModel); + ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel); ChatModel configureChatModel(MistralAiChatModel chatModel); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java index 068d75873b..20fb379f7d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.ai.provider; public enum AiProvider { OPENAI, + AZURE_OPENAI, GOOGLE_AI_GEMINI, MISTRAL_AI diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java index b7c594ba60..84cda054d0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java @@ -25,11 +25,12 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; ) @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 = MistralAiProviderConfig.class, name = "MISTRAL_AI") }) public sealed interface AiProviderConfig - permits OpenAiProviderConfig, GoogleAiGeminiProviderConfig, MistralAiProviderConfig { + permits OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig, MistralAiProviderConfig { AiProvider provider(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java new file mode 100644 index 0000000000..f9a2a98a21 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java @@ -0,0 +1,30 @@ +/** + * 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.provider; + +public record AzureOpenAiProviderConfig(String apiKey) implements AiProviderConfig { + + @Override + public AiProvider provider() { + return AiProvider.AZURE_OPENAI; + } + + @Override + public String apiKey() { + return apiKey; + } + +} From 815ac9fef2d42a20f2c889c89b72df7bc895443b Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 24 Jun 2025 11:11:02 +0300 Subject: [PATCH 084/435] Fix invalid resource info caching --- .../controller/TbResourceControllerTest.java | 58 +++++++++++++++++++ .../resourceInfo/ResourceInfoCacheKey.java | 4 +- .../dao/resource/BaseResourceService.java | 6 +- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java index 569ff840ca..3b194de7b4 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java @@ -202,6 +202,60 @@ public class TbResourceControllerTest extends AbstractControllerTest { Assert.assertEquals(savedResource.getFileName(), foundResource.getFileName()); } + @Test + public void testFindSystemResourceInfoById() throws Exception { + loginSysAdmin(); + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My system resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setEncodedData(TEST_DATA); + TbResourceInfo savedResourceInfo = save(resource); + assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME); + + TbResourceInfo resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + loginTenantAdmin(); + resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + + loginSysAdmin(); + resource = new TbResource(savedResourceInfo); + resource.setFileName(DEFAULT_FILE_NAME_2); + resource.setEncodedData(TEST_DATA); + savedResourceInfo = save(resource); + assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME_2); + + resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + loginTenantAdmin(); + resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + } + + @Test + public void testFindTenantResourceInfoById() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My tenant resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setEncodedData(TEST_DATA); + TbResourceInfo savedResourceInfo = save(resource); + assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME); + + TbResourceInfo resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + + resource = new TbResource(savedResourceInfo); + resource.setFileName(DEFAULT_FILE_NAME_2); + resource.setEncodedData(TEST_DATA); + savedResourceInfo = save(resource); + assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME_2); + + resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + } + @Test public void testDeleteTbResource() throws Exception { TbResource resource = new TbResource(); @@ -878,6 +932,10 @@ public class TbResourceControllerTest extends AbstractControllerTest { }); } + private TbResourceInfo findResourceInfo(TbResourceId id) throws Exception { + return doGet("/api/resource/info/" + id, TbResourceInfo.class); + } + private byte[] download(TbResourceId resourceId) throws Exception { return doGet("/api/resource/" + resourceId + "/download") .andExpect(status().isOk()) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java b/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java index cb19b36059..e1877ebe4e 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java @@ -20,7 +20,6 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.thingsboard.server.common.data.id.TbResourceId; -import org.thingsboard.server.common.data.id.TenantId; import java.io.Serial; import java.io.Serializable; @@ -34,12 +33,11 @@ public class ResourceInfoCacheKey implements Serializable { @Serial private static final long serialVersionUID = 2100510964692846992L; - private final TenantId tenantId; private final TbResourceId tbResourceId; @Override public String toString() { - return tenantId + "_" + tbResourceId; + return tbResourceId.toString(); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index bf73c59708..bf941256f5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -263,7 +263,7 @@ public class BaseResourceService extends AbstractCachedEntityService INCORRECT_RESOURCE_ID + id); - return cache.getAndPutInTransaction(new ResourceInfoCacheKey(tenantId, resourceId), + return cache.getAndPutInTransaction(new ResourceInfoCacheKey(resourceId), () -> resourceInfoDao.findById(tenantId, resourceId.getId()), true); } @@ -712,7 +712,7 @@ public class BaseResourceService extends AbstractCachedEntityService Date: Tue, 24 Jun 2025 11:55:03 +0300 Subject: [PATCH 085/435] CalculatedField functionality support for Edge - changed logic for sync CalculatedField --- .../service/edge/rpc/EdgeGrpcSession.java | 6 + .../edge/rpc/processor/BaseEdgeProcessor.java | 11 -- .../processor/asset/AssetEdgeProcessor.java | 2 - .../BaseCalculatedFieldProcessor.java | 2 +- .../processor/device/DeviceEdgeProcessor.java | 2 - .../rpc/sync/DefaultEdgeRequestsService.java | 139 +++++++++++++++++- .../edge/rpc/sync/EdgeRequestsService.java | 4 + .../server/dao/cf/CalculatedFieldService.java | 2 - common/edge-api/src/main/proto/edge.proto | 7 + .../dao/cf/BaseCalculatedFieldService.java | 13 +- .../server/dao/cf/CalculatedFieldDao.java | 3 +- .../dao/sql/cf/JpaCalculatedFieldDao.java | 6 +- 12 files changed, 160 insertions(+), 37 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 7430062bd7..3339ebb60e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -49,6 +49,7 @@ import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg; import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg; import org.thingsboard.server.gen.edge.v1.ConnectResponseCode; @@ -882,6 +883,11 @@ public abstract class EdgeGrpcSession implements Closeable { result.add(ctx.getEdgeRequestsService().processRelationRequestMsg(edge.getTenantId(), edge, relationRequestMsg)); } } + if (uplinkMsg.getCalculatedFieldRequestMsgCount() > 0) { + for (CalculatedFieldRequestMsg calculatedFieldRequestMsg : uplinkMsg.getCalculatedFieldRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processCalculatedFieldRequestMsg(edge.getTenantId(), edge, calculatedFieldRequestMsg)); + } + } if (uplinkMsg.getUserCredentialsRequestMsgCount() > 0) { for (UserCredentialsRequestMsg userCredentialsRequestMsg : uplinkMsg.getUserCredentialsRequestMsgList()) { result.add(ctx.getEdgeRequestsService().processUserCredentialsRequestMsg(edge.getTenantId(), edge, userCredentialsRequestMsg)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 077ff21d26..6fcb02e4bc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -25,7 +25,6 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -54,13 +53,11 @@ import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; import org.thingsboard.server.dao.entity.EntityDaoRegistry; -import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.service.edge.EdgeContextComponent; -import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.state.DefaultDeviceStateService; @@ -384,12 +381,4 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor { }); } - protected List getCalculatedFieldUpdateMsgs(TenantId tenantId, EntityId entityId) { - List calculatedFields = edgeCtx.getCalculatedFieldService().findCalculatedFieldsByEntityId(tenantId, entityId); - - return calculatedFields.stream() - .map(calculatedField -> EdgeMsgConstructorUtils.constructCalculatedFieldUpdatedMsg(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, calculatedField)) - .toList(); - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index 538a90f7d7..cba2b62af1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -120,8 +120,6 @@ public class AssetEdgeProcessor extends BaseAssetProcessor implements AssetProce .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addAssetUpdateMsg(assetUpdateMsg); - getCalculatedFieldUpdateMsgs(edgeEvent.getTenantId(), assetId).forEach(builder::addCalculatedFieldUpdateMsg); - if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { AssetProfile assetProfile = edgeCtx.getAssetProfileService().findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId()); builder.addAssetProfileUpdateMsg(EdgeMsgConstructorUtils.constructAssetProfileUpdatedMsg(msgType, assetProfile)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java index a7781fe55d..503e5d74dc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java @@ -80,7 +80,7 @@ public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { calculatedField.setId(calculatedFieldId); } - edgeCtx.getCalculatedFieldService().save(calculatedField, false); + edgeCtx.getCalculatedFieldService().save(calculatedField); } catch (Exception e) { log.error("[{}] Failed to process calculatedField update msg [{}]", tenantId, calculatedFieldUpdateMsg, e); throw e; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index be2a04626c..763821f11c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -244,8 +244,6 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr builder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg).build(); } - getCalculatedFieldUpdateMsgs(edgeEvent.getTenantId(), deviceId).forEach(builder::addCalculatedFieldUpdateMsg); - if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { DeviceProfile deviceProfile = edgeCtx.getDeviceProfileService().findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); builder.addDeviceProfileUpdateMsg(EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index 6fc6d5bad9..4487a0acf8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -23,6 +23,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -46,6 +48,8 @@ import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; @@ -53,13 +57,19 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.asset.AssetProfileService; +import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.cf.CalculatedFieldService; +import org.thingsboard.server.dao.device.DeviceProfileService; +import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; import org.thingsboard.server.gen.edge.v1.RelationRequestMsg; @@ -72,10 +82,16 @@ import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.state.DefaultDeviceStateService; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; @Service @TbCoreComponent @@ -90,7 +106,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { @Autowired private TimeseriesService timeseriesService; - + @Autowired private RelationService relationService; @@ -104,6 +120,17 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { @Autowired private WidgetTypeService widgetTypeService; + @Autowired + private CalculatedFieldService calculatedFieldService; + @Autowired + private DeviceService deviceService; + @Autowired + private DeviceProfileService deviceProfileService; + @Autowired + private AssetService assetService; + @Autowired + private AssetProfileService assetProfileService; + @Autowired private DbCallbackExecutorService dbCallbackExecutorService; @@ -293,6 +320,116 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { return futureToSet; } + @Override + public ListenableFuture processCalculatedFieldRequestMsg(TenantId tenantId, Edge edge, CalculatedFieldRequestMsg calculatedFieldRequestMsg) { + log.trace("[{}] processCalculatedFieldRequestMsg [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg); + + EntityId entityId = EntityIdFactory.getByTypeAndUuid( + EntityType.valueOf(calculatedFieldRequestMsg.getEntityType()), + new UUID(calculatedFieldRequestMsg.getEntityIdMSB(), calculatedFieldRequestMsg.getEntityIdLSB())); + + if (entityId.getEntityType() == EntityType.EDGE) { + log.trace("[{}] processAllCalculatedField [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg); + return syncAllCalculatedFieldForEdge(tenantId, edge, calculatedFieldRequestMsg); + } else { + log.trace("[{}] processCalculatedField [{}][{}] for entity [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg, entityId.getEntityType(), entityId.getId()); + return saveCalculatedFieldsToEdge(tenantId, edge.getId(), entityId); + } + } + + @NotNull + private ListenableFuture syncAllCalculatedFieldForEdge(TenantId tenantId, Edge edge, CalculatedFieldRequestMsg calculatedFieldRequestMsg) { + EdgeId edgeId = edge.getId(); + ListenableFuture> deviceIdsFuture = + findAllEntityIdsAsync(pageLink -> deviceService.findDevicesByTenantIdAndEdgeId(tenantId, edgeId, pageLink)); + ListenableFuture> assetIdsFuture = + findAllEntityIdsAsync(pageLink -> assetService.findAssetsByTenantIdAndEdgeId(tenantId, edgeId, pageLink)); + ListenableFuture> deviceProfileIdsFuture = + findAllEntityIdsAsync(pageLink -> deviceProfileService.findDeviceProfiles(tenantId, pageLink)); + ListenableFuture> assetProfileIdsFuture = + findAllEntityIdsAsync(pageLink -> assetProfileService.findAssetProfiles(tenantId, pageLink)); + + ListenableFuture> allEntityIdFuture = + mergeEntityIdFutures(deviceIdsFuture, assetIdsFuture, deviceProfileIdsFuture, assetProfileIdsFuture); + + return Futures.transformAsync(allEntityIdFuture, allIds -> { + log.trace("[{}][{}] Going to sync calculatedFields for [{}] entities", tenantId, edge.getName(), allIds.size()); + List> saveFutures = allIds.stream() + .map(id -> saveCalculatedFieldsToEdge(tenantId, edge.getId(), id)) + .collect(Collectors.toList()); + + return Futures.transform( + Futures.allAsList(saveFutures), + result -> null, + dbCallbackExecutorService + ); + }, dbCallbackExecutorService); + } + + private > ListenableFuture> findAllEntityIdsAsync(Function> fetcher) { + return dbCallbackExecutorService.submit(() -> { + List result = new ArrayList<>(); + PageLink pageLink = new PageLink(100); + PageData pageData; + while (true) { + pageData = fetcher.apply(pageLink); + + Optional.ofNullable(pageData) + .map(PageData::getData) + .ifPresent(dataList -> dataList.stream() + .filter(Objects::nonNull) + .map(ExportableEntity::getId) + .forEach(result::add)); + + if (pageData == null || !pageData.hasNext()) { + break; + } + + pageLink = pageLink.nextPageLink(); + } + return result; + }); + } + + @SafeVarargs + private ListenableFuture> mergeEntityIdFutures(ListenableFuture>... futures) { + return Futures.transform( + Futures.allAsList(Arrays.asList(futures)), + listsOfIds -> listsOfIds.stream() + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .collect(Collectors.toList()), + dbCallbackExecutorService + ); + } + + private ListenableFuture saveCalculatedFieldsToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) { + return Futures.transformAsync( + dbCallbackExecutorService.submit(() -> calculatedFieldService.findCalculatedFieldsByEntityId(tenantId, entityId)), + calculatedFields -> { + log.trace("[{}][{}][{}][{}] calculatedField(s) are going to be pushed to edge.", tenantId, edgeId, entityId, calculatedFields.size()); + + List> futures = calculatedFields.stream().map(calculatedField -> { + try { + return saveEdgeEvent(tenantId, edgeId, EdgeEventType.CALCULATED_FIELD, + EdgeEventActionType.ADDED, calculatedField.getId(), JacksonUtil.valueToTree(calculatedField)); + } catch (Exception e) { + String errMsg = String.format("[%s][%s] Exception during loading calculatedField [%s] to edge on sync!", tenantId, edgeId, calculatedField); + log.error(errMsg, e); + return Futures.immediateFailedFuture(e); + } + }).collect(Collectors.toList()); + + return Futures.transform( + Futures.allAsList(futures), + voids -> null, + dbCallbackExecutorService + ); + }, + dbCallbackExecutorService + ); + } + private ListenableFuture> findRelationByQuery(TenantId tenantId, Edge edge, EntityId entityId, EntitySearchDirection direction) { EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(new RelationsSearchParameters(entityId, direction, 1, false)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java index 2a115eeace..a147a7054b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; import org.thingsboard.server.gen.edge.v1.RelationRequestMsg; @@ -35,6 +36,8 @@ public interface EdgeRequestsService { ListenableFuture processRelationRequestMsg(TenantId tenantId, Edge edge, RelationRequestMsg relationRequestMsg); + ListenableFuture processCalculatedFieldRequestMsg(TenantId tenantId, Edge edge, CalculatedFieldRequestMsg calculatedFieldRequestMsg); + @Deprecated(since = "3.9.1", forRemoval = true) ListenableFuture processDeviceCredentialsRequestMsg(TenantId tenantId, Edge edge, DeviceCredentialsRequestMsg deviceCredentialsRequestMsg); @@ -46,4 +49,5 @@ public interface EdgeRequestsService { @Deprecated(since = "3.9.1", forRemoval = true) ListenableFuture processEntityViewsRequestMsg(TenantId tenantId, Edge edge, EntityViewsRequestMsg entityViewsRequestMsg); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java index 85cd8d24fd..5ac0e98d41 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java @@ -31,8 +31,6 @@ public interface CalculatedFieldService extends EntityDaoService { CalculatedField save(CalculatedField calculatedField); - CalculatedField save(CalculatedField calculatedField, boolean doValidate); - CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId); CalculatedField findByEntityIdAndName(EntityId entityId, String name); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 23eafc4574..c805f42e8c 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -334,6 +334,12 @@ message RelationRequestMsg { string entityType = 3; } +message CalculatedFieldRequestMsg { + int64 entityIdMSB = 1; + int64 entityIdLSB = 2; + string entityType = 3; +} + // DEPRECATED. FOR REMOVAL message UserCredentialsRequestMsg { option deprecated = true; @@ -433,6 +439,7 @@ message UplinkMsg { repeated RuleChainUpdateMsg ruleChainUpdateMsg = 23; repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24; repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25; + repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26; } message UplinkResponseMsg { diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 793f839fb9..0add13b5f8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -62,17 +62,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return doSave(calculatedField, oldCalculatedField); } - @Override - public CalculatedField save(CalculatedField calculatedField, boolean doValidate) { - CalculatedField oldCalculatedField = null; - - if (doValidate) { - oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); - } - - return doSave(calculatedField, oldCalculatedField); - } - private CalculatedField doSave(CalculatedField calculatedField, CalculatedField oldCalculatedField) { try { TenantId tenantId = calculatedField.getTenantId(); @@ -104,7 +93,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements log.trace("Executing findByEntityIdAndName [{}], calculatedFieldName[{}]", entityId, name); validateId(entityId.getId(), id -> INCORRECT_ENTITY_ID + id); - return calculatedFieldDao.findByEntityIdAndName(entityId, name).orElse(null); + return calculatedFieldDao.findByEntityIdAndName(entityId, name); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java index bd244b9d24..d5465cb8a1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java @@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import java.util.List; -import java.util.Optional; public interface CalculatedFieldDao extends Dao { @@ -36,7 +35,7 @@ public interface CalculatedFieldDao extends Dao { List findAll(); - Optional findByEntityIdAndName(EntityId entityId, String name); + CalculatedField findByEntityIdAndName(EntityId entityId, String name); PageData findAll(PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java index 23a146bc47..2632b0237b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java @@ -34,7 +34,6 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.List; -import java.util.Optional; import java.util.UUID; @Slf4j @@ -67,9 +66,8 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findByEntityIdAndName(EntityId entityId, String name) { - CalculatedField calculatedField = DaoUtil.getData(calculatedFieldRepository.findByEntityIdAndName(entityId.getId(), name)); - return Optional.ofNullable(calculatedField); + public CalculatedField findByEntityIdAndName(EntityId entityId, String name) { + return DaoUtil.getData(calculatedFieldRepository.findByEntityIdAndName(entityId.getId(), name)); } @Override From a67f2eb516c02abc17d70cb49cb3b29f7d65826f Mon Sep 17 00:00:00 2001 From: yevhenii Date: Tue, 24 Jun 2025 13:37:34 +0300 Subject: [PATCH 086/435] CalculatedField functionality support for Edge - refactoring --- .../edge/rpc/processor/BaseEdgeProcessor.java | 4 +- .../BaseCalculatedFieldProcessor.java | 2 +- .../rpc/sync/DefaultEdgeRequestsService.java | 84 +------------------ .../server/dao/cf/CalculatedFieldService.java | 2 + .../dao/cf/BaseCalculatedFieldService.java | 14 ++++ 5 files changed, 21 insertions(+), 85 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 6fcb02e4bc..e49f22ed7e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -139,8 +139,8 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor { UPDATED_COMMENT, DELETED -> true; default -> switch (type) { case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, - WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, NOTIFICATION_TEMPLATE, NOTIFICATION_TARGET, - NOTIFICATION_RULE -> true; + WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, NOTIFICATION_TEMPLATE, + NOTIFICATION_TARGET, NOTIFICATION_RULE -> true; default -> false; }; }; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java index 503e5d74dc..a7781fe55d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java @@ -80,7 +80,7 @@ public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { calculatedField.setId(calculatedFieldId); } - edgeCtx.getCalculatedFieldService().save(calculatedField); + edgeCtx.getCalculatedFieldService().save(calculatedField, false); } catch (Exception e) { log.error("[{}] Failed to process calculatedField update msg [{}]", tenantId, calculatedFieldUpdateMsg, e); throw e; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index 4487a0acf8..414dcdab76 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -23,7 +23,6 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; -import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -32,7 +31,6 @@ import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -48,8 +46,6 @@ import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; @@ -82,15 +78,10 @@ import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.state.DefaultDeviceStateService; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Optional; import java.util.UUID; -import java.util.function.Function; import java.util.stream.Collectors; @Service @@ -328,79 +319,8 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { EntityType.valueOf(calculatedFieldRequestMsg.getEntityType()), new UUID(calculatedFieldRequestMsg.getEntityIdMSB(), calculatedFieldRequestMsg.getEntityIdLSB())); - if (entityId.getEntityType() == EntityType.EDGE) { - log.trace("[{}] processAllCalculatedField [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg); - return syncAllCalculatedFieldForEdge(tenantId, edge, calculatedFieldRequestMsg); - } else { - log.trace("[{}] processCalculatedField [{}][{}] for entity [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg, entityId.getEntityType(), entityId.getId()); - return saveCalculatedFieldsToEdge(tenantId, edge.getId(), entityId); - } - } - - @NotNull - private ListenableFuture syncAllCalculatedFieldForEdge(TenantId tenantId, Edge edge, CalculatedFieldRequestMsg calculatedFieldRequestMsg) { - EdgeId edgeId = edge.getId(); - ListenableFuture> deviceIdsFuture = - findAllEntityIdsAsync(pageLink -> deviceService.findDevicesByTenantIdAndEdgeId(tenantId, edgeId, pageLink)); - ListenableFuture> assetIdsFuture = - findAllEntityIdsAsync(pageLink -> assetService.findAssetsByTenantIdAndEdgeId(tenantId, edgeId, pageLink)); - ListenableFuture> deviceProfileIdsFuture = - findAllEntityIdsAsync(pageLink -> deviceProfileService.findDeviceProfiles(tenantId, pageLink)); - ListenableFuture> assetProfileIdsFuture = - findAllEntityIdsAsync(pageLink -> assetProfileService.findAssetProfiles(tenantId, pageLink)); - - ListenableFuture> allEntityIdFuture = - mergeEntityIdFutures(deviceIdsFuture, assetIdsFuture, deviceProfileIdsFuture, assetProfileIdsFuture); - - return Futures.transformAsync(allEntityIdFuture, allIds -> { - log.trace("[{}][{}] Going to sync calculatedFields for [{}] entities", tenantId, edge.getName(), allIds.size()); - List> saveFutures = allIds.stream() - .map(id -> saveCalculatedFieldsToEdge(tenantId, edge.getId(), id)) - .collect(Collectors.toList()); - - return Futures.transform( - Futures.allAsList(saveFutures), - result -> null, - dbCallbackExecutorService - ); - }, dbCallbackExecutorService); - } - - private > ListenableFuture> findAllEntityIdsAsync(Function> fetcher) { - return dbCallbackExecutorService.submit(() -> { - List result = new ArrayList<>(); - PageLink pageLink = new PageLink(100); - PageData pageData; - while (true) { - pageData = fetcher.apply(pageLink); - - Optional.ofNullable(pageData) - .map(PageData::getData) - .ifPresent(dataList -> dataList.stream() - .filter(Objects::nonNull) - .map(ExportableEntity::getId) - .forEach(result::add)); - - if (pageData == null || !pageData.hasNext()) { - break; - } - - pageLink = pageLink.nextPageLink(); - } - return result; - }); - } - - @SafeVarargs - private ListenableFuture> mergeEntityIdFutures(ListenableFuture>... futures) { - return Futures.transform( - Futures.allAsList(Arrays.asList(futures)), - listsOfIds -> listsOfIds.stream() - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .collect(Collectors.toList()), - dbCallbackExecutorService - ); + log.trace("[{}] processCalculatedField [{}][{}] for entity [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg, entityId.getEntityType(), entityId.getId()); + return saveCalculatedFieldsToEdge(tenantId, edge.getId(), entityId); } private ListenableFuture saveCalculatedFieldsToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java index 5ac0e98d41..85cd8d24fd 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java @@ -31,6 +31,8 @@ public interface CalculatedFieldService extends EntityDaoService { CalculatedField save(CalculatedField calculatedField); + CalculatedField save(CalculatedField calculatedField, boolean doValidate); + CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId); CalculatedField findByEntityIdAndName(EntityId entityId, String name); diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 0add13b5f8..dc6abbde8b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -62,6 +62,20 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return doSave(calculatedField, oldCalculatedField); } + @Override + public CalculatedField save(CalculatedField calculatedField, boolean doValidate) { + CalculatedField oldCalculatedField = null; + + if (doValidate) { + oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); + } else if (calculatedField.getId() != null) { + oldCalculatedField = findById(calculatedField.getTenantId(), calculatedField.getId()); + } + + return doSave(calculatedField, oldCalculatedField); + } + + private CalculatedField doSave(CalculatedField calculatedField, CalculatedField oldCalculatedField) { try { TenantId tenantId = calculatedField.getTenantId(); From 2414b979233c5b7fa621c8bb3dec2f5dea8125de Mon Sep 17 00:00:00 2001 From: yevhenii Date: Tue, 24 Jun 2025 14:21:23 +0300 Subject: [PATCH 087/435] CalculatedField functionality support for Edge - add test --- .../rpc/sync/DefaultEdgeRequestsService.java | 12 ------ .../server/edge/CalculatedFieldEdgeTest.java | 38 +++++++++++++++++++ .../thingsboard/edge/rpc/EdgeGrpcClient.java | 2 +- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index 414dcdab76..7cdf916439 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -53,12 +53,8 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.dao.asset.AssetProfileService; -import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.cf.CalculatedFieldService; -import org.thingsboard.server.dao.device.DeviceProfileService; -import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -113,14 +109,6 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { @Autowired private CalculatedFieldService calculatedFieldService; - @Autowired - private DeviceService deviceService; - @Autowired - private DeviceProfileService deviceProfileService; - @Autowired - private AssetService assetService; - @Autowired - private AssetProfileService assetProfileService; @Autowired private DbCallbackExecutorService dbCallbackExecutorService; diff --git a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java index 0eabba10f9..268e19345c 100644 --- a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedField import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg; import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; @@ -114,6 +115,43 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { checkCalculatedFieldOnCloud(uplinkMsg, uuid, calculatedField.getName()); } + @Test + public void testSendCalculatedFieldRequestToCloud() throws Exception { + Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); + + // create calculatedField + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); + + edgeImitator.expectMessageAmount(1); + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + CalculatedFieldRequestMsg.Builder calculatedFieldRequestMsgBuilder = CalculatedFieldRequestMsg.newBuilder(); + calculatedFieldRequestMsgBuilder.setEntityIdMSB(savedDevice.getId().getId().getMostSignificantBits()); + calculatedFieldRequestMsgBuilder.setEntityIdLSB(savedDevice.getId().getId().getLeastSignificantBits()); + calculatedFieldRequestMsgBuilder.setEntityType(savedDevice.getId().getEntityType().name()); + testAutoGeneratedCodeByProtobuf(calculatedFieldRequestMsgBuilder); + + uplinkMsgBuilder.addCalculatedFieldRequestMsg(calculatedFieldRequestMsgBuilder.build()); + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); + CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; + CalculatedField calculatedFieldFromEdge = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); + Assert.assertNotNull(calculatedFieldFromEdge); + Assert.assertEquals(savedCalculatedField, calculatedFieldFromEdge); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); + } + @Test public void testUpdateCalculatedFieldNameOnCloud() throws Exception { Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index 509b30feb4..e1ff386a32 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -136,7 +136,7 @@ public class EdgeGrpcClient implements EdgeRpcClient { .setConnectRequestMsg(ConnectRequestMsg.newBuilder() .setEdgeRoutingKey(edgeKey) .setEdgeSecret(edgeSecret) - .setEdgeVersion(EdgeVersion.V_4_0_0) + .setEdgeVersion(EdgeVersion.V_4_1_0) .setMaxInboundMessageSize(maxInboundMessageSize) .build()) .build()); From 7f0cd6dccbae3ac095c3b485a1938551ed76de1e Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 24 Jun 2025 15:08:17 +0300 Subject: [PATCH 088/435] AI rule node: support more OpenAI models --- .../server/common/data/ai/model/AiModel.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java index 686c1f4fcf..5955ac4355 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java @@ -30,6 +30,16 @@ import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; ) @JsonSubTypes({ @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4o"), + @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4o-mini"), + @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4.1"), + @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4.1-mini"), + @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4.1-nano"), + @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o4-mini"), + // @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o3-pro"), needs verification with Gov ID :) + // @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o3"), needs verification with Gov ID :) + @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o3-mini"), + // @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o1-pro"), LC4j sends requests to v1/chat/completions, but o1-pro is only supported in v1/responses + @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o1"), @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.5-flash"), @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "mistral-medium-latest") }) From aca9e3186e0d6e171160b8fc1188524d278eb8f5 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 24 Jun 2025 16:42:49 +0300 Subject: [PATCH 089/435] AI rule node: support more Gemini models --- .../server/common/data/ai/model/AiModel.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java index 5955ac4355..9fc4d50985 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java @@ -29,18 +29,24 @@ import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; visible = true ) @JsonSubTypes({ - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4o"), - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4o-mini"), - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4.1"), - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4.1-mini"), - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4.1-nano"), @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o4-mini"), // @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o3-pro"), needs verification with Gov ID :) // @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o3"), needs verification with Gov ID :) @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o3-mini"), // @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o1-pro"), LC4j sends requests to v1/chat/completions, but o1-pro is only supported in v1/responses @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o1"), + @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4.1"), + @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4.1-mini"), + @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4.1-nano"), + @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4o"), + @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4o-mini"), + @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.5-pro"), @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.5-flash"), + @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.0-flash"), + @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.0-flash-lite"), + @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-1.5-pro"), + @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-1.5-flash"), + @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-1.5-flash-8b"), @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "mistral-medium-latest") }) public interface AiModel> { From ec64201dfc778fbb821a03b377d792aad8c94c3c Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 24 Jun 2025 16:56:02 +0300 Subject: [PATCH 090/435] UI: Fixed vc result popover with long error message --- ui-ngx/src/app/modules/home/components/vc/version-control.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/vc/version-control.scss b/ui-ngx/src/app/modules/home/components/vc/version-control.scss index 35a0932f19..f221fc66ad 100644 --- a/ui-ngx/src/app/modules/home/components/vc/version-control.scss +++ b/ui-ngx/src/app/modules/home/components/vc/version-control.scss @@ -16,9 +16,11 @@ :host { .vc-result-message { max-width: 65vw; + max-height: 65vh; padding: 0 8px; text-align: center; word-wrap: break-word; + overflow: auto; &:first-child { padding-top: 48px; } From fe63059a776ef943925218acd46cf83978f3c1b0 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 24 Jun 2025 17:52:40 +0300 Subject: [PATCH 091/435] AI rule node: support more Mistral AI models --- .../server/common/data/ai/model/AiModel.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java index 9fc4d50985..8f88b7aa01 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; visible = true ) @JsonSubTypes({ + // OpenAI models @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o4-mini"), // @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o3-pro"), needs verification with Gov ID :) // @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o3"), needs verification with Gov ID :) @@ -40,6 +41,8 @@ import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4.1-nano"), @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4o"), @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4o-mini"), + + // Google AI Gemini models @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.5-pro"), @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.5-flash"), @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.0-flash"), @@ -47,7 +50,17 @@ import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-1.5-pro"), @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-1.5-flash"), @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-1.5-flash-8b"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "mistral-medium-latest") + + // Mistral AI models + @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "magistral-medium-latest"), + @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "magistral-small-latest"), + @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "mistral-large-latest"), + @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "mistral-medium-latest"), + @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "mistral-small-latest"), + @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "pixtral-large-latest"), + @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "ministral-8b-latest"), + @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "ministral-3b-latest"), + @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "open-mistral-nemo") }) public interface AiModel> { From 536f252b1636a5dfa8631f8d9efc83b48d6e813e Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 24 Jun 2025 18:47:03 +0300 Subject: [PATCH 092/435] AI rule node: add index on tenant ID --- application/src/main/data/upgrade/basic/schema_update.sql | 6 ++++-- dao/src/main/resources/sql/schema-entities-idx.sql | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 7a33c481c2..4897a35b41 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -60,7 +60,7 @@ WHERE trigger_type = 'RATE_LIMITS' -- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS END --- CREATE AI MODEL SETTINGS TABLE START +-- CREATE AI MODEL SETTINGS TABLE AND INDEX START CREATE TABLE ai_model_settings ( id UUID NOT NULL PRIMARY KEY, @@ -74,4 +74,6 @@ CREATE TABLE ai_model_settings ( CONSTRAINT ai_model_settings_external_id_unq_key UNIQUE (tenant_id, external_id) ); --- CREATE AI MODEL SETTINGS TABLE END +CREATE INDEX idx_ai_model_settings_tenant_id ON ai_model_settings(tenant_id); + +-- CREATE AI MODEL SETTINGS TABLE AND INDEX END diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index ad311f00df..5611be2121 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -131,3 +131,5 @@ CREATE INDEX IF NOT EXISTS idx_resource_type_public_resource_key ON resource(res CREATE INDEX IF NOT EXISTS mobile_app_bundle_tenant_id ON mobile_app_bundle(tenant_id); CREATE INDEX IF NOT EXISTS idx_job_tenant_id ON job(tenant_id); + +CREATE INDEX IF NOT EXISTS idx_ai_model_settings_tenant_id ON ai_model_settings(tenant_id); From 76f33663c97efa3e9e9267922d84768f91594107 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 24 Jun 2025 11:21:29 +0300 Subject: [PATCH 093/435] Added missing fields for RPC message in cluster mode --- .../org/thingsboard/server/common/util/ProtoUtils.java | 10 +++++++--- common/proto/src/main/proto/queue.proto | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index bc96952ca7..3cded5a491 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -522,7 +522,7 @@ public class ProtoUtils { } private static TransportProtos.ToDeviceRpcRequestActorMsgProto toProto(ToDeviceRpcRequestActorMsg msg) { - TransportProtos.ToDeviceRpcRequestMsg proto = TransportProtos.ToDeviceRpcRequestMsg.newBuilder() + TransportProtos.ToDeviceRpcRequestMsg.Builder builder = TransportProtos.ToDeviceRpcRequestMsg.newBuilder() .setMethodName(msg.getMsg().getBody().getMethod()) .setParams(msg.getMsg().getBody().getParams()) .setExpirationTime(msg.getMsg().getExpirationTime()) @@ -530,7 +530,11 @@ public class ProtoUtils { .setRequestIdLSB(msg.getMsg().getId().getLeastSignificantBits()) .setOneway(msg.getMsg().isOneway()) .setPersisted(msg.getMsg().isPersisted()) - .build(); + .setAdditionalInfo(msg.getMsg().getAdditionalInfo()); + if (msg.getMsg().getRetries() != null) { + builder.setRetries(msg.getMsg().getRetries()); + } + TransportProtos.ToDeviceRpcRequestMsg proto = builder.build(); return TransportProtos.ToDeviceRpcRequestActorMsgProto.newBuilder() .setTenantIdMSB(msg.getTenantId().getId().getMostSignificantBits()) @@ -551,7 +555,7 @@ public class ProtoUtils { toDeviceRpcRequestMsg.getOneway(), toDeviceRpcRequestMsg.getExpirationTime(), new ToDeviceRpcRequestBody(toDeviceRpcRequestMsg.getMethodName(), toDeviceRpcRequestMsg.getParams()), - toDeviceRpcRequestMsg.getPersisted(), 0, ""); + toDeviceRpcRequestMsg.getPersisted(), toDeviceRpcRequestMsg.hasRetries() ? toDeviceRpcRequestMsg.getRetries() : null, toDeviceRpcRequestMsg.getAdditionalInfo()); return new ToDeviceRpcRequestActorMsg(proto.getServiceId(), toDeviceRpcRequest); } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index c413ecb3c1..2667838b60 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -696,6 +696,8 @@ message ToDeviceRpcRequestMsg { int64 requestIdLSB = 6; bool oneway = 7; bool persisted = 8; + optional int32 retries = 9; + string additionalInfo = 10; } message ToDeviceRpcResponseMsg { From 71ec0a588a402faa0a644cd5a7e5c617b354c56f Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 25 Jun 2025 11:34:17 +0300 Subject: [PATCH 094/435] Fix test to use profile with ota on import --- .../service/sync/vc/VersionControlTest.java | 25 +++++++++++-------- .../data/sync/ie/OtaPackageExportData.java | 4 +++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index 5c5b7bb107..461ca5a2ec 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -685,23 +685,28 @@ public class VersionControlTest extends AbstractControllerTest { } @Test - public void testOtaPackageVc_betweenTenants() throws Exception { + public void testOtaPackageVcWithProfile_betweenTenants() throws Exception { DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile v1.0"); OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE); + deviceProfile.setFirmwareId(firmware.getId()); + deviceProfile.setSoftwareId(software.getId()); + deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); String versionId = createVersion("ota packages", EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE); - OtaPackage firmwareOta = findOtaPackage(firmware.getTitle()); - OtaPackage softwareOta = findOtaPackage(software.getTitle()); - loginTenant2(); loadVersion(versionId, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE); - OtaPackage importedFirmwareOta = findOtaPackage(firmwareOta.getTitle()); - OtaPackage importedSoftwareOta = findOtaPackage(softwareOta.getTitle()); - checkImportedEntity(tenantId1, firmwareOta, tenantId2, importedFirmwareOta); - checkImportedOtaPackageData(firmwareOta, importedFirmwareOta); - checkImportedEntity(tenantId1, softwareOta, tenantId2, importedSoftwareOta); - checkImportedOtaPackageData(softwareOta, importedSoftwareOta); + DeviceProfile importedProfile = findDeviceProfile(deviceProfile.getName()); + OtaPackage importedFirmwareOta = findOtaPackage(firmware.getTitle()); + OtaPackage importedSoftwareOta = findOtaPackage(software.getTitle()); + checkImportedEntity(tenantId1, deviceProfile, tenantId2, importedProfile); + checkImportedDeviceProfileData(deviceProfile, importedProfile); + checkImportedEntity(tenantId1, firmware, tenantId2, importedFirmwareOta); + checkImportedOtaPackageData(firmware, importedFirmwareOta); + checkImportedEntity(tenantId1, software, tenantId2, importedSoftwareOta); + checkImportedOtaPackageData(software, importedSoftwareOta); + assertThat(importedProfile.getFirmwareId()).isEqualTo(importedFirmwareOta.getId()); + assertThat(importedProfile.getSoftwareId()).isEqualTo(importedSoftwareOta.getId()); } protected void checkImportedOtaPackageData(OtaPackage otaPackage, OtaPackage importedOtaPackage) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java index df12bb5b08..44e2f7857c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java @@ -22,6 +22,10 @@ import org.thingsboard.server.common.data.OtaPackage; @EqualsAndHashCode(callSuper = true) public class OtaPackageExportData extends EntityExportData { + /* + * OtaPackage is not a versioned entity; its 'version' field is part of the domain model (not used for optimistic locking) + * We override both methods to ensure 'version' is not ignored during (de)serialization. + */ @JsonIgnoreProperties(value = {"tenantId", "createdTime"}, ignoreUnknown = true) @Override public OtaPackage getEntity() { From 3ae97e0d7e1cf6c5d31e989923c1fcfc31affafd Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Wed, 25 Jun 2025 12:11:51 +0300 Subject: [PATCH 095/435] CalculatedField functionality support for Edge - refactoring --- .../service/edge/EdgeContextComponent.java | 2 +- .../edge/rpc/processor/BaseEdgeProcessor.java | 8 ++--- .../BaseCalculatedFieldProcessor.java | 33 +------------------ .../CalculatedFieldEdgeProcessor.java | 6 ++-- .../CalculatedFieldProcessor.java | 2 +- .../rpc/sync/DefaultEdgeRequestsService.java | 6 ++-- .../dao/cf/BaseCalculatedFieldService.java | 4 --- .../server/dao/edge/EdgeServiceImpl.java | 1 - 8 files changed, 12 insertions(+), 50 deletions(-) rename application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/{calculated => cf}/BaseCalculatedFieldProcessor.java (66%) rename application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/{calculated => cf}/CalculatedFieldEdgeProcessor.java (96%) rename application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/{calculated => cf}/CalculatedFieldProcessor.java (94%) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index abdc89ebd9..1193f935a0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -62,7 +62,7 @@ import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmProcessor; import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor; import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.asset.profile.AssetProfileEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.calculated.CalculatedFieldProcessor; +import org.thingsboard.server.service.edge.rpc.processor.cf.CalculatedFieldProcessor; import org.thingsboard.server.service.edge.rpc.processor.dashboard.DashboardEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.device.DeviceEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.device.profile.DeviceProfileEdgeProcessor; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index e49f22ed7e..dc16c5b229 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -222,7 +222,7 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor { if (edgeId != null) { return saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body); } else { - return processNotificationToRelatedEdges(tenantId, entityId, type, actionType, originatorEdgeId); + return processNotificationToRelatedEdges(tenantId, entityId, entityId, type, actionType, originatorEdgeId); } case DELETED: EdgeEventActionType deleted = EdgeEventActionType.DELETED; @@ -260,11 +260,11 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor { } } - private ListenableFuture processNotificationToRelatedEdges(TenantId tenantId, EntityId entityId, EdgeEventType type, - EdgeEventActionType actionType, EdgeId sourceEdgeId) { + protected ListenableFuture processNotificationToRelatedEdges(TenantId tenantId, EntityId ownerEntityId, EntityId entityId, EdgeEventType type, + EdgeEventActionType actionType, EdgeId sourceEdgeId) { List> futures = new ArrayList<>(); PageDataIterableByTenantIdEntityId edgeIds = - new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, entityId, RELATED_EDGES_CACHE_ITEMS); + new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, ownerEntityId, RELATED_EDGES_CACHE_ITEMS); for (EdgeId relatedEdgeId : edgeIds) { if (!relatedEdgeId.equals(sourceEdgeId)) { futures.add(saveEdgeEvent(tenantId, relatedEdgeId, type, actionType, entityId, null)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java similarity index 66% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java index a7781fe55d..4ef6ec7ba2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java @@ -13,33 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.processor.calculated; +package org.thingsboard.server.service.edge.rpc.processor.cf; import com.datastax.oss.driver.api.core.uuid.Uuids; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.util.Pair; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; -import java.util.ArrayList; -import java.util.List; - -import static org.thingsboard.server.dao.edge.BaseRelatedEdgesService.RELATED_EDGES_CACHE_ITEMS; - @Slf4j public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { @@ -88,23 +76,4 @@ public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { return Pair.of(isCreated, isNameUpdated); } - protected ListenableFuture pushEventToAllRelatedEdges(TenantId tenantId, EntityId calculatedFieldOwnerId, EntityId entityId, EdgeEventType type, EdgeEventActionType actionType, EdgeId sourceEdgeId) { - List> futures = new ArrayList<>(); - PageDataIterableByTenantIdEntityId edgeIds = - new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, calculatedFieldOwnerId, RELATED_EDGES_CACHE_ITEMS); - for (EdgeId relatedEdgeId : edgeIds) { - if (!relatedEdgeId.equals(sourceEdgeId)) { - futures.add(saveEdgeEvent(tenantId, relatedEdgeId, type, actionType, entityId, null)); - } - } - return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); - } - - protected ListenableFuture pushEventToAllEdges(TenantId tenantId, EdgeEventType type, EdgeEventActionType actionType, EntityId entityId, EdgeId sourceEdgeId) { - return switch (actionType) { - case ADDED, UPDATED, DELETED -> processActionForAllEdges(tenantId, type, actionType, entityId, null, sourceEdgeId); - default -> Futures.immediateFuture(null); - }; - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java similarity index 96% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java index a0212bb4df..cab4b5ecc1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.processor.calculated; +package org.thingsboard.server.service.edge.rpc.processor.cf; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; @@ -134,9 +134,9 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i return edgeId != null ? saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body) : - pushEventToAllRelatedEdges(tenantId, calculatedFieldOwnerId, entityId, type, actionType, originatorEdgeId); + processNotificationToRelatedEdges(tenantId, calculatedFieldOwnerId, entityId, type, actionType, originatorEdgeId); } else { - return pushEventToAllEdges(tenantId, type, actionType, entityId, originatorEdgeId); + return processActionForAllEdges(tenantId, type, actionType, entityId, null, originatorEdgeId); } default: return super.processEntityNotification(tenantId, edgeNotificationMsg); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldProcessor.java similarity index 94% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldProcessor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldProcessor.java index ba9c8b27e1..d21af858f0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldProcessor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.processor.calculated; +package org.thingsboard.server.service.edge.rpc.processor.cf; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.edge.Edge; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index 7cdf916439..66ff05b45a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -78,7 +78,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.stream.Collectors; @Service @TbCoreComponent @@ -322,11 +321,10 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { return saveEdgeEvent(tenantId, edgeId, EdgeEventType.CALCULATED_FIELD, EdgeEventActionType.ADDED, calculatedField.getId(), JacksonUtil.valueToTree(calculatedField)); } catch (Exception e) { - String errMsg = String.format("[%s][%s] Exception during loading calculatedField [%s] to edge on sync!", tenantId, edgeId, calculatedField); - log.error(errMsg, e); + log.error("[{}][{}] Exception during loading calculatedField [{}] to edge on sync!", tenantId, edgeId, calculatedField, e); return Futures.immediateFailedFuture(e); } - }).collect(Collectors.toList()); + }).toList(); return Futures.transform( Futures.allAsList(futures), diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index dc6abbde8b..c0cb886747 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -58,20 +58,17 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements @Override public CalculatedField save(CalculatedField calculatedField) { CalculatedField oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); - return doSave(calculatedField, oldCalculatedField); } @Override public CalculatedField save(CalculatedField calculatedField, boolean doValidate) { CalculatedField oldCalculatedField = null; - if (doValidate) { oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); } else if (calculatedField.getId() != null) { oldCalculatedField = findById(calculatedField.getTenantId(), calculatedField.getId()); } - return doSave(calculatedField, oldCalculatedField); } @@ -106,7 +103,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements public CalculatedField findByEntityIdAndName(EntityId entityId, String name) { log.trace("Executing findByEntityIdAndName [{}], calculatedFieldName[{}]", entityId, name); validateId(entityId.getId(), id -> INCORRECT_ENTITY_ID + id); - return calculatedFieldDao.findByEntityIdAndName(entityId, name); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index ebcfd7678a..0655d05572 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -524,7 +524,6 @@ public class EdgeServiceImpl extends AbstractCachedEntityService Date: Wed, 25 Jun 2025 14:31:41 +0300 Subject: [PATCH 096/435] AI rule node: add support for Google Vertex AI Gemini --- application/pom.xml | 4 + .../Langchain4jChatModelConfigurerImpl.java | 59 ++++++++- .../server/common/data/ai/model/AiModel.java | 47 +------ .../common/data/ai/model/AiModelConfig.java | 6 +- .../data/ai/model/AiModelTypeIdResolver.java | 120 ++++++++++++++++++ .../data/ai/model/chat/AiChatModel.java | 2 +- .../data/ai/model/chat/AiChatModelConfig.java | 2 +- .../ai/model/chat/AzureOpenAiChatModel.java | 10 +- .../model/chat/GoogleAiGeminiChatModel.java | 10 +- .../chat/GoogleVertexAiGeminiChatModel.java | 60 +++++++++ .../chat/Langchain4jChatModelConfigurer.java | 2 + .../ai/model/chat/MistralAiChatModel.java | 10 +- .../data/ai/model/chat/OpenAiChatModel.java | 10 +- .../common/data/ai/provider/AiProvider.java | 1 + .../data/ai/provider/AiProviderConfig.java | 5 +- .../provider/AzureOpenAiProviderConfig.java | 5 - .../GoogleAiGeminiProviderConfig.java | 5 - .../GoogleVertexAiGeminiProviderConfig.java | 31 +++++ .../ai/provider/MistralAiProviderConfig.java | 5 - .../ai/provider/OpenAiProviderConfig.java | 5 - .../dao/sql/ai/AiModelSettingsRepository.java | 30 +++-- 21 files changed, 326 insertions(+), 103 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java diff --git a/application/pom.xml b/application/pom.xml index 2c11c50d3d..7ec326758b 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -393,6 +393,10 @@ dev.langchain4j langchain4j-google-ai-gemini + + dev.langchain4j + langchain4j-vertex-ai-gemini + dev.langchain4j langchain4j-mistral-ai diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index d3b23b65b5..91e9c7874e 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -15,25 +15,37 @@ */ package org.thingsboard.server.service.ai; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.cloud.vertexai.Transport; +import com.google.cloud.vertexai.VertexAI; +import com.google.cloud.vertexai.api.GenerationConfig; +import com.google.cloud.vertexai.generativeai.GenerativeModel; import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModel; 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.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.provider.GoogleVertexAiGeminiProviderConfig; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.time.Duration; @Component -public class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer { +class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer { @Override public ChatModel configureChatModel(OpenAiChatModel chatModel) { OpenAiChatModel.Config modelConfig = chatModel.modelConfig(); return dev.langchain4j.model.openai.OpenAiChatModel.builder() .apiKey(chatModel.providerConfig().apiKey()) - .modelName(chatModel.modelId()) + .modelName(modelConfig.modelId()) .temperature(modelConfig.temperature()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) @@ -45,7 +57,7 @@ public class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelC AzureOpenAiChatModel.Config modelConfig = chatModel.modelConfig(); return dev.langchain4j.model.azure.AzureOpenAiChatModel.builder() .apiKey(chatModel.providerConfig().apiKey()) - .deploymentName(chatModel.modelId()) + .deploymentName(modelConfig.modelId()) .temperature(modelConfig.temperature()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) @@ -57,19 +69,56 @@ public class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelC GoogleAiGeminiChatModel.Config modelConfig = chatModel.modelConfig(); return dev.langchain4j.model.googleai.GoogleAiGeminiChatModel.builder() .apiKey(chatModel.providerConfig().apiKey()) - .modelName(chatModel.modelId()) + .modelName(modelConfig.modelId()) .temperature(modelConfig.temperature()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); } + @Override + public ChatModel configureChatModel(GoogleVertexAiGeminiChatModel chatModel) { + GoogleVertexAiGeminiProviderConfig providerConfig = chatModel.providerConfig(); + GoogleVertexAiGeminiChatModel.Config modelConfig = chatModel.modelConfig(); + + // construct service account credentials using service account key JSON + ObjectNode serviceAccountKeyJson = providerConfig.serviceAccountKey(); + ServiceAccountCredentials serviceAccountCredentials; + try { + serviceAccountCredentials = ServiceAccountCredentials + .fromStream(new ByteArrayInputStream(JacksonUtil.writeValueAsBytes(serviceAccountKeyJson))); + } catch (IOException e) { + throw new RuntimeException("Failed to parse service account key JSON", e); + } + + // construct Vertex AI instance + var vertexAI = new VertexAI.Builder() + .setProjectId(providerConfig.projectId()) + .setLocation(providerConfig.location()) + .setCredentials(serviceAccountCredentials) + .setTransport(Transport.REST) // GRPC also possible, but likely does not work with service account keys + .build(); + + // map model config to generation config + var generationConfigBuilder = GenerationConfig.newBuilder(); + if (modelConfig.temperature() != null) { + generationConfigBuilder.setTemperature(modelConfig.temperature().floatValue()); + } + var generationConfig = generationConfigBuilder.build(); + + // construct generative model instance + var generativeModel = new GenerativeModel(modelConfig.modelId(), vertexAI) + .withGenerationConfig(generationConfig); + + return new VertexAiGeminiChatModel(generativeModel, generationConfig, modelConfig.maxRetries()); + } + @Override public ChatModel configureChatModel(MistralAiChatModel chatModel) { MistralAiChatModel.Config modelConfig = chatModel.modelConfig(); return dev.langchain4j.model.mistralai.MistralAiChatModel.builder() .apiKey(chatModel.providerConfig().apiKey()) - .modelName(chatModel.modelId()) + .modelName(modelConfig.modelId()) .temperature(modelConfig.temperature()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java index 8f88b7aa01..114fe2a528 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java @@ -15,61 +15,22 @@ */ package org.thingsboard.server.common.data.ai.model; -import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModel; -import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModel; -import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModel; +import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, + use = JsonTypeInfo.Id.CUSTOM, include = JsonTypeInfo.As.PROPERTY, - property = "modelId", - visible = true + property = "@type" ) -@JsonSubTypes({ - // OpenAI models - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o4-mini"), - // @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o3-pro"), needs verification with Gov ID :) - // @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o3"), needs verification with Gov ID :) - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o3-mini"), - // @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o1-pro"), LC4j sends requests to v1/chat/completions, but o1-pro is only supported in v1/responses - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o1"), - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4.1"), - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4.1-mini"), - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4.1-nano"), - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4o"), - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4o-mini"), - - // Google AI Gemini models - @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.5-pro"), - @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.5-flash"), - @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.0-flash"), - @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.0-flash-lite"), - @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-1.5-pro"), - @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-1.5-flash"), - @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-1.5-flash-8b"), - - // Mistral AI models - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "magistral-medium-latest"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "magistral-small-latest"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "mistral-large-latest"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "mistral-medium-latest"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "mistral-small-latest"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "pixtral-large-latest"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "ministral-8b-latest"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "ministral-3b-latest"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "open-mistral-nemo") -}) +@JsonTypeIdResolver(AiModelTypeIdResolver.class) public interface AiModel> { AiProviderConfig providerConfig(); AiModelType modelType(); - String modelId(); - C modelConfig(); AiModel withModelConfig(C config); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java index b07007c301..7f9526a4db 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java @@ -15,4 +15,8 @@ */ package org.thingsboard.server.common.data.ai.model; -public interface AiModelConfig> {} +public interface AiModelConfig> { + + String modelId(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java new file mode 100644 index 0000000000..34dab4b6aa --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java @@ -0,0 +1,120 @@ +/** + * 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.JsonTypeInfo; +import com.fasterxml.jackson.databind.DatabindContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase; +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 java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public final class AiModelTypeIdResolver extends TypeIdResolverBase { + + private static final Map> typeIdToModelClass; + + static { + Map> map = new HashMap<>(); + + // OpenAI models + map.put("OPENAI::o4-mini", OpenAiChatModel.class); + // map.put("OPENAI::o3-pro", OpenAiChatModel.class); // needs verification with Gov ID :) + // map.put("OPENAI::o3", OpenAiChatModel.class); // needs verification with Gov ID :) + map.put("OPENAI::o3-mini", OpenAiChatModel.class); + // map.put("OPENAI::o1-pro", OpenAiChatModel.class); // LC4j sends requests to v1/chat/completions, but o1-pro is only supported in v1/responses + map.put("OPENAI::o1", OpenAiChatModel.class); + map.put("OPENAI::gpt-4.1", OpenAiChatModel.class); + map.put("OPENAI::gpt-4.1-mini", OpenAiChatModel.class); + map.put("OPENAI::gpt-4.1-nano", OpenAiChatModel.class); + map.put("OPENAI::gpt-4o", OpenAiChatModel.class); + map.put("OPENAI::gpt-4o-mini", OpenAiChatModel.class); + + // Google AI Gemini models + map.put("GOOGLE_AI_GEMINI::gemini-2.5-pro", GoogleAiGeminiChatModel.class); + map.put("GOOGLE_AI_GEMINI::gemini-2.5-flash", GoogleAiGeminiChatModel.class); + map.put("GOOGLE_AI_GEMINI::gemini-2.0-flash", GoogleAiGeminiChatModel.class); + map.put("GOOGLE_AI_GEMINI::gemini-2.0-flash-lite", GoogleAiGeminiChatModel.class); + map.put("GOOGLE_AI_GEMINI::gemini-1.5-pro", GoogleAiGeminiChatModel.class); + map.put("GOOGLE_AI_GEMINI::gemini-1.5-flash", GoogleAiGeminiChatModel.class); + map.put("GOOGLE_AI_GEMINI::gemini-1.5-flash-8b", GoogleAiGeminiChatModel.class); + + // Google Vertex AI Gemini models + map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-2.5-pro", GoogleVertexAiGeminiChatModel.class); + map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-2.5-flash", GoogleVertexAiGeminiChatModel.class); + map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-2.0-flash", GoogleVertexAiGeminiChatModel.class); + map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-2.0-flash-lite", GoogleVertexAiGeminiChatModel.class); + map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-1.5-pro", GoogleVertexAiGeminiChatModel.class); + map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-1.5-flash", GoogleVertexAiGeminiChatModel.class); + map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-1.5-flash-8b", GoogleVertexAiGeminiChatModel.class); + + // Mistral AI models + map.put("MISTRAL_AI::magistral-medium-latest", MistralAiChatModel.class); + map.put("MISTRAL_AI::magistral-small-latest", MistralAiChatModel.class); + map.put("MISTRAL_AI::mistral-large-latest", MistralAiChatModel.class); + map.put("MISTRAL_AI::mistral-medium-latest", MistralAiChatModel.class); + map.put("MISTRAL_AI::mistral-small-latest", MistralAiChatModel.class); + map.put("MISTRAL_AI::pixtral-large-latest", MistralAiChatModel.class); + map.put("MISTRAL_AI::ministral-8b-latest", MistralAiChatModel.class); + map.put("MISTRAL_AI::ministral-3b-latest", MistralAiChatModel.class); + map.put("MISTRAL_AI::open-mistral-nemo", MistralAiChatModel.class); + + typeIdToModelClass = Collections.unmodifiableMap(map); + } + + private JavaType baseType; + + @Override + public void init(JavaType baseType) { + this.baseType = baseType; + } + + @Override + public String idFromValue(Object value) { + return generateId((AiModel) value); + } + + @Override + public String idFromValueAndType(Object value, Class suggestedType) { + return generateId((AiModel) value); + } + + @Override + public JavaType typeFromId(DatabindContext context, String id) { + Class modelClass = typeIdToModelClass.get(id); + if (modelClass == null) { + throw new IllegalArgumentException("Unknown model type ID: " + id); + } + return context.constructSpecializedType(baseType, modelClass); + } + + @Override + public JsonTypeInfo.Id getMechanism() { + return JsonTypeInfo.Id.CUSTOM; + } + + private static String generateId(AiModel model) { + String provider = model.providerConfig().provider().name(); + String modelId = model.modelConfig().modelId(); + return provider + "::" + modelId; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java index 4e944ed6ec..08d4630d10 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java @@ -20,7 +20,7 @@ import org.thingsboard.server.common.data.ai.model.AiModel; import org.thingsboard.server.common.data.ai.model.AiModelType; public sealed interface AiChatModel> extends AiModel - permits OpenAiChatModel, AzureOpenAiChatModel, GoogleAiGeminiChatModel, MistralAiChatModel { + permits OpenAiChatModel, AzureOpenAiChatModel, GoogleAiGeminiChatModel, GoogleVertexAiGeminiChatModel, MistralAiChatModel { ChatModel configure(Langchain4jChatModelConfigurer configurer); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java index e44c9c5fa1..4ef5369005 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java @@ -18,7 +18,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import org.thingsboard.server.common.data.ai.model.AiModelConfig; public sealed interface AiChatModelConfig> extends AiModelConfig - permits OpenAiChatModel.Config, AzureOpenAiChatModel.Config, GoogleAiGeminiChatModel.Config, MistralAiChatModel.Config { + permits OpenAiChatModel.Config, AzureOpenAiChatModel.Config, GoogleAiGeminiChatModel.Config, GoogleVertexAiGeminiChatModel.Config, MistralAiChatModel.Config { Double temperature(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java index 37a1d84095..9920b546f0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java @@ -20,11 +20,11 @@ import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; public record AzureOpenAiChatModel( AzureOpenAiProviderConfig providerConfig, - String modelId, Config modelConfig ) implements AiChatModel { public record Config( + String modelId, Double temperature, Integer timeoutSeconds, Integer maxRetries @@ -32,17 +32,17 @@ public record AzureOpenAiChatModel( @Override public AzureOpenAiChatModel.Config withTemperature(Double temperature) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } @Override public AzureOpenAiChatModel.Config withTimeoutSeconds(Integer timeoutSeconds) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } @Override public AzureOpenAiChatModel.Config withMaxRetries(Integer maxRetries) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } } @@ -54,7 +54,7 @@ public record AzureOpenAiChatModel( @Override public AzureOpenAiChatModel withModelConfig(AzureOpenAiChatModel.Config config) { - return new AzureOpenAiChatModel(providerConfig, modelId, config); + return new AzureOpenAiChatModel(providerConfig, config); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java index 875262abb6..c09903b305 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java @@ -20,11 +20,11 @@ import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConf public record GoogleAiGeminiChatModel( GoogleAiGeminiProviderConfig providerConfig, - String modelId, Config modelConfig ) implements AiChatModel { public record Config( + String modelId, Double temperature, Integer timeoutSeconds, Integer maxRetries @@ -32,17 +32,17 @@ public record GoogleAiGeminiChatModel( @Override public Config withTemperature(Double temperature) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } @Override public Config withTimeoutSeconds(Integer timeoutSeconds) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } @Override public Config withMaxRetries(Integer maxRetries) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } } @@ -54,7 +54,7 @@ public record GoogleAiGeminiChatModel( @Override public GoogleAiGeminiChatModel withModelConfig(GoogleAiGeminiChatModel.Config config) { - return new GoogleAiGeminiChatModel(providerConfig, modelId, config); + return new GoogleAiGeminiChatModel(providerConfig, config); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java new file mode 100644 index 0000000000..a340430828 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; + +public record GoogleVertexAiGeminiChatModel( + GoogleVertexAiGeminiProviderConfig providerConfig, + Config modelConfig +) implements AiChatModel { + + public record Config( + String modelId, + Double temperature, + Integer timeoutSeconds, // TODO: not supported by Vertex AI + Integer maxRetries + ) implements AiChatModelConfig { + + @Override + public Config withTemperature(Double temperature) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + @Override + public Config withTimeoutSeconds(Integer timeoutSeconds) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + @Override + public Config withMaxRetries(Integer maxRetries) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public GoogleVertexAiGeminiChatModel withModelConfig(GoogleVertexAiGeminiChatModel.Config config) { + return new GoogleVertexAiGeminiChatModel(providerConfig, config); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java index 41dd1fe4ad..3602fb14a7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java @@ -25,6 +25,8 @@ public interface Langchain4jChatModelConfigurer { ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel); + ChatModel configureChatModel(GoogleVertexAiGeminiChatModel chatModel); + ChatModel configureChatModel(MistralAiChatModel chatModel); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java index 413d2b93a8..e4eae1b766 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java @@ -20,11 +20,11 @@ import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; public record MistralAiChatModel( MistralAiProviderConfig providerConfig, - String modelId, Config modelConfig ) implements AiChatModel { public record Config( + String modelId, Double temperature, Integer timeoutSeconds, Integer maxRetries @@ -32,17 +32,17 @@ public record MistralAiChatModel( @Override public Config withTemperature(Double temperature) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } @Override public Config withTimeoutSeconds(Integer timeoutSeconds) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } @Override public Config withMaxRetries(Integer maxRetries) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } } @@ -54,7 +54,7 @@ public record MistralAiChatModel( @Override public MistralAiChatModel withModelConfig(Config config) { - return new MistralAiChatModel(providerConfig, modelId, config); + return new MistralAiChatModel(providerConfig, config); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java index 0d5031d512..a4d7401cbf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java @@ -20,11 +20,11 @@ import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; public record OpenAiChatModel( OpenAiProviderConfig providerConfig, - String modelId, Config modelConfig ) implements AiChatModel { public record Config( + String modelId, Double temperature, Integer timeoutSeconds, Integer maxRetries @@ -32,17 +32,17 @@ public record OpenAiChatModel( @Override public OpenAiChatModel.Config withTemperature(Double temperature) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } @Override public OpenAiChatModel.Config withTimeoutSeconds(Integer timeoutSeconds) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } @Override public OpenAiChatModel.Config withMaxRetries(Integer maxRetries) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } } @@ -54,7 +54,7 @@ public record OpenAiChatModel( @Override public OpenAiChatModel withModelConfig(OpenAiChatModel.Config config) { - return new OpenAiChatModel(providerConfig, modelId, config); + return new OpenAiChatModel(providerConfig, config); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java index 20fb379f7d..da4df1a076 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java @@ -20,6 +20,7 @@ public enum AiProvider { OPENAI, AZURE_OPENAI, GOOGLE_AI_GEMINI, + GOOGLE_VERTEX_AI_GEMINI, MISTRAL_AI } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java index 84cda054d0..e3b3b250cc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java @@ -27,13 +27,12 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @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") }) public sealed interface AiProviderConfig - permits OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig, MistralAiProviderConfig { + permits OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig, GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig { AiProvider provider(); - String apiKey(); - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java index f9a2a98a21..0b948eabab 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java @@ -22,9 +22,4 @@ public record AzureOpenAiProviderConfig(String apiKey) implements AiProviderConf return AiProvider.AZURE_OPENAI; } - @Override - public String apiKey() { - return apiKey; - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java index 0bb9d21b52..35def6f0f5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java @@ -22,9 +22,4 @@ public record GoogleAiGeminiProviderConfig(String apiKey) implements AiProviderC return AiProvider.GOOGLE_AI_GEMINI; } - @Override - public String apiKey() { - return apiKey; - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java new file mode 100644 index 0000000000..a5140279ac --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java @@ -0,0 +1,31 @@ +/** + * 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.provider; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +public record GoogleVertexAiGeminiProviderConfig( + String projectId, + String location, + ObjectNode serviceAccountKey +) implements AiProviderConfig { + + @Override + public AiProvider provider() { + return AiProvider.GOOGLE_VERTEX_AI_GEMINI; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java index 45e3b68800..29c251b3cd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java @@ -22,9 +22,4 @@ public record MistralAiProviderConfig(String apiKey) implements AiProviderConfig return AiProvider.MISTRAL_AI; } - @Override - public String apiKey() { - return apiKey; - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java index 0536d1176e..6c069276d5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java @@ -22,9 +22,4 @@ public record OpenAiProviderConfig(String apiKey) implements AiProviderConfig { return AiProvider.OPENAI; } - @Override - public String apiKey() { - return apiKey; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java index e5909c5818..a86d5ba5c8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java @@ -35,15 +35,27 @@ interface AiModelSettingsRepository extends JpaRepository findByTenantIdAndName(UUID tenantId, String name); - @Query(nativeQuery = true, value = """ - SELECT * - FROM ai_model_settings ai_model - WHERE ai_model.tenant_id = :tenantId - AND (:textSearch IS NULL - OR ai_model.name ILIKE '%' || :textSearch || '%' - OR (ai_model.configuration -> 'providerConfig' ->> 'provider') ILIKE '%' || :textSearch || '%' - OR (ai_model.configuration ->> 'modelId') ILIKE '%' || :textSearch || '%') - """) + @Query( + value = """ + SELECT * + FROM ai_model_settings ai_model + WHERE ai_model.tenant_id = :tenantId + AND (:textSearch IS NULL + OR ai_model.name ILIKE '%' || :textSearch || '%' + OR (ai_model.configuration -> 'providerConfig' ->> 'provider') ILIKE '%' || :textSearch || '%' + OR (ai_model.configuration -> 'modelConfig' ->> 'modelId') ILIKE '%' || :textSearch || '%') + """, + countQuery = """ + SELECT COUNT(*) + FROM ai_model_settings ai_model + WHERE ai_model.tenant_id = :tenantId + AND (:textSearch IS NULL + OR ai_model.name ILIKE '%' || :textSearch || '%' + OR (ai_model.configuration -> 'providerConfig' ->> 'provider') ILIKE '%' || :textSearch || '%' + OR (ai_model.configuration -> 'modelConfig' ->> 'modelId') ILIKE '%' || :textSearch || '%') + """, + nativeQuery = true + ) Page 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") From d8afecd3dc3d25013733acbcdffe807aeadb6c6a Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 25 Jun 2025 14:49:15 +0300 Subject: [PATCH 097/435] UI: Add new unit in/s and capitalize unit name --- ui-ngx/src/app/shared/models/units/speed.ts | 7 +- .../assets/locale/locale.constant-en_US.json | 205 +++++++++--------- 2 files changed, 109 insertions(+), 103 deletions(-) diff --git a/ui-ngx/src/app/shared/models/units/speed.ts b/ui-ngx/src/app/shared/models/units/speed.ts index b930b0739c..3e3bad7b03 100644 --- a/ui-ngx/src/app/shared/models/units/speed.ts +++ b/ui-ngx/src/app/shared/models/units/speed.ts @@ -19,7 +19,7 @@ import { TbMeasure, TbMeasureUnits } from '@shared/models/unit.models'; export type SpeedUnits = SpeedMetricUnits | SpeedImperialUnits; export type SpeedMetricUnits = 'm/s' | 'km/h' | 'mm/min' | 'mm/s'; -export type SpeedImperialUnits = 'mph' | 'kt' | 'ft/s' | 'ft/min' | 'in/h'; +export type SpeedImperialUnits = 'mph' | 'kt' | 'ft/s' | 'ft/min' | 'in/s' | 'in/h'; const METRIC: TbMeasureUnits = { ratio: 1 / 1.609344, @@ -70,6 +70,11 @@ const IMPERIAL: TbMeasureUnits = { tags: ['velocity', 'pace'], to_anchor: 0.0113636, }, + 'in/s': { + name: 'unit.inch-per-second', + tags: ['velocity', 'pace'], + to_anchor: 0.0568182, + }, 'in/h': { name: 'unit.inch-per-hour', tags: ['velocity', 'pace'], diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 06a461b412..fe07c52ea8 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6014,9 +6014,9 @@ "foot-us": "Foot (US survey)", "yard": "Yard", "mile": "Mile", - "nautical-mile": "Nautical Mile", - "astronomical-unit": "Astronomical Unit", - "reciprocal-metre": "Reciprocal Metre", + "nautical-mile": "Nautical mile", + "astronomical-unit": "Astronomical unit", + "reciprocal-metre": "Reciprocal metre", "meter-per-meter": "Meter per meter", "steradian": "Steradian", "thou": "Thou", @@ -6046,24 +6046,24 @@ "quarter": "Quarter", "slug": "Slug", "carat": "Carat", - "cubic-millimeter": "Cubic Millimeter", - "cubic-centimeter": "Cubic Centimeter", - "cubic-meter": "Cubic Meter", - "cubic-kilometer": "Cubic Kilometer", + "cubic-millimeter": "Cubic millimeter", + "cubic-centimeter": "Cubic centimeter", + "cubic-meter": "Cubic meter", + "cubic-kilometer": "Cubic kilometer", "microliter": "Microliter", "milliliter": "Milliliter", "liter": "Liter", "hectoliter": "Hectolitre", - "cubic-inch": "Cubic Inch", - "cubic-foot": "Cubic Foot", - "cubic-yard": "Cubic Yard", - "fluid-ounce": "Fluid Ounce", - "fluid-ounce-per-second": "Fluid Ounce per second", + "cubic-inch": "Cubic inch", + "cubic-foot": "Cubic foot", + "cubic-yard": "Cubic yard", + "fluid-ounce": "Fluid ounce", + "fluid-ounce-per-second": "Fluid ounce per second", "pint": "Pint", "quart": "Quart", "gallon": "Gallon", - "oil-barrels": "Oil Barrel", - "cubic-meter-per-kilogram": "Cubic Meter per Kilogram", + "oil-barrels": "Oil barrel", + "cubic-meter-per-kilogram": "Cubic meter per kilogram", "gill": "Gill", "hogshead": "Hogshead", "teaspoon": "Teaspoon", @@ -6074,13 +6074,14 @@ "rankine": "Rankine", "fahrenheit": "Fahrenheit", "percent": "Percent", - "meter-per-second": "Meter per Second", - "kilometer-per-hour": "Kilometer per Hour", - "foot-per-second": "Foot per Second", - "foot-per-minute": "Foot per Minute", - "mile-per-hour": "Mile per Hour", + "meter-per-second": "Meter per second", + "kilometer-per-hour": "Kilometer per hour", + "foot-per-second": "Foot per second", + "foot-per-minute": "Foot per minute", + "mile-per-hour": "Mile per hour", "knot": "Knot", - "inch-per-hour": "Inch per Hour", + "inch-per-second": "Inch per second", + "inch-per-hour": "Inch per hour", "millimeters-per-minute": "Millimeters per minute", "kilometer-per-hour-squared": "Kilometer per hour squared", "foot-per-second-squared": "Foot per second squared", @@ -6097,18 +6098,18 @@ "inch-pounds": "Inch-pounds", "newton-per-meter": "Newton per meter", "atmospheres": "Atmospheres", - "pounds-per-square-inch": "Pounds per Square Inch", - "kilopound-per-square-inch": "Kilopound per Square Inch", + "pounds-per-square-inch": "Pounds per square inch", + "kilopound-per-square-inch": "Kilopound per square inch", "torr": "Torr", - "inches-of-mercury": "Inches of Mercury", - "pascal-per-square-meter": "Pascal per Square Meter", - "pound-per-square-inch": "Pound per Square Inch", - "newton-per-square-meter": "Newton per Square Meter", - "kilogram-force-per-square-meter": "Kilogram-force per Square Meter", - "pascal-per-square-centimeter": "Pascal per Square Centimeter", - "ton-force-per-square-inch": "Ton-force per Square Inch", - "kilonewton-per-square-meter": "Kilonewton per Square Meter", - "newton-per-square-millimeter": "Newton per Square Millimeter", + "inches-of-mercury": "Inches of mercury", + "pascal-per-square-meter": "Pascal per square meter", + "pound-per-square-inch": "Pound per square inch", + "newton-per-square-meter": "Newton per square meter", + "kilogram-force-per-square-meter": "Kilogram-force per square meter", + "pascal-per-square-centimeter": "Pascal per square centimeter", + "ton-force-per-square-inch": "Ton-force per square inch", + "kilonewton-per-square-meter": "Kilonewton per square meter", + "newton-per-square-millimeter": "Newton per square millimeter", "microjoule": "Microjoule", "millijoule": "Millijoule", "joule": "Joule", @@ -6122,31 +6123,31 @@ "megawatt-hour": "Megawatt-hour", "gigawatt-hour": "Gigawatt-hour", "electron-volts": "Electron volts", - "joules-per-coulomb": "Joules per Coulomb", - "british-thermal-unit": "British Thermal Units", - "thousand-british-thermal-unit": "Thousand British Thermal Units", - "million-british-thermal-unit": "Million British Thermal Units", + "joules-per-coulomb": "Joules per coulomb", + "british-thermal-unit": "British thermal units", + "thousand-british-thermal-unit": "Thousand British thermal units", + "million-british-thermal-unit": "Million British thermal units", "foot-pound": "Foot-pound", "calorie": "Calorie", - "small-calorie": "Small Calorie", + "small-calorie": "Small calorie", "kilocalorie": "Kilocalorie", - "joule-per-kelvin": "Joule per Kelvin", - "joule-per-kilogram-kelvin": "Joule per Kilogram-Kelvin", - "joule-per-kilogram": "Joule per Kilogram", - "watt-per-meter-kelvin": "Watt per Meter-Kelvin", - "joule-per-cubic-meter": "Joule per Cubic Meter", + "joule-per-kelvin": "Joule per kelvin", + "joule-per-kilogram-kelvin": "Joule per kilogram-kelvin", + "joule-per-kilogram": "Joule per kilogram", + "watt-per-meter-kelvin": "Watt per meter-kelvin", + "joule-per-cubic-meter": "Joule per cubic meter", "therm": "Therm", - "electric-dipole-moment": "Electric Dipole Moment", - "magnetic-dipole-moment": "Magnetic Dipole Moment", + "electric-dipole-moment": "Electric dipole moment", + "magnetic-dipole-moment": "Magnetic dipole moment", "debye": "Debye", - "coulomb-per-square-meter-per-volt": "Coulomb per Square Meter per Volt", + "coulomb-per-square-meter-per-volt": "Coulomb per square meter per volt", "milliwatt": "Milliwatt", "microwatt": "Microwatt", "watt": "Watt", "kilowatt": "Kilowatt", "megawatt": "Megawatt", "gigawatt": "Gigawatt", - "metric-horsepower": "Metric Horsepower", + "metric-horsepower": "Metric horsepower", "milliwatt-per-square-centimeter": "Milliwatts per square centimeter", "watt-per-square-centimeter": "Watts per square centimeter", "kilowatt-per-square-centimeter": "Kilowatts per square centimeter", @@ -6165,28 +6166,28 @@ "mmbtu-per-hour": "Million British thermal units per hour", "mmbtu-per-second": "Million British thermal units per second", "mmbtu-per-day": "Million British thermal units per day", - "foot-pound-per-second": "foot-pound per second", + "foot-pound-per-second": "Foot-pound per second", "coulomb": "Coulomb", "millicoulomb": "Millicoulombs", "microcoulomb": "Microcoulomb", "nanocoulomb": "Nanocoulomb", "picocoulomb": "Picocoulomb", "coulomb-per-meter": "Coulomb per meter", - "coulomb-per-cubic-meter": "Coulomb per Cubic Meter", - "coulomb-per-square-meter": "Coulomb per Square Meter", - "square-millimeter": "Square Millimeter", - "square-centimeter": "Square Centimeter", - "square-meter": "Square Meter", + "coulomb-per-cubic-meter": "Coulomb per cubic meter", + "coulomb-per-square-meter": "Coulomb per square meter", + "square-millimeter": "Square millimeter", + "square-centimeter": "Square centimeter", + "square-meter": "Square meter", "hectare": "Hectare", - "square-kilometer": "Square Kilometer", - "square-inch": "Square Inch", - "square-foot": "Square Foot", - "square-yard": "Square Yard", + "square-kilometer": "Square kilometer", + "square-inch": "Square inch", + "square-foot": "Square foot", + "square-yard": "Square yard", "acre": "Acre", - "square-mile": "Square Mile", + "square-mile": "Square mile", "are": "Are", "barn": "Barn", - "circular-inch": "Circular Inch", + "circular-inch": "Circular inch", "milliampere-hour": "Milliampere-hour", "ampere-hours": "Ampere-hours", "kiloampere-hours": "Kiloampere-hours", @@ -6199,11 +6200,11 @@ "megaampere": "Megaampere", "gigaampere": "Gigaampere", "microampere-per-square-centimeter": "Microampere per square centimeter", - "ampere-per-square-meter": "Ampere per Square Meter", - "ampere-per-meter": "Ampere per Meter", + "ampere-per-square-meter": "Ampere per square meter", + "ampere-per-meter": "Ampere per meter", "oersted": "Oersted", - "bohr-magneton": "Bohr Magneton", - "ampere-meter-squared": "Ampere-Meter Squared", + "bohr-magneton": "Bohr magneton", + "ampere-meter-squared": "Ampere-meter squared", "nanovolt": "Nanovolt", "picovolt": "Picovolt", "millivolt": "Millivolts", @@ -6213,12 +6214,12 @@ "megavolt": "Megavolt", "dbmV": "Decibel volt", "dbm": "Decibel-milliwatts", - "volt-meter": "Volt-Meter", - "kilovolt-meter": "Kilovolt-Meter", - "megavolt-meter": "Megavolt-Meter", - "microvolt-meter": "Microvolt-Meter", - "millivolt-meter": "Millivolt-Meter", - "nanovolt-meter": "Nanovolt-Meter", + "volt-meter": "Volt-meter", + "kilovolt-meter": "Kilovolt-meter", + "megavolt-meter": "Megavolt-meter", + "microvolt-meter": "Microvolt-meter", + "millivolt-meter": "Millivolt-meter", + "nanovolt-meter": "Nanovolt-meter", "ohm": "Ohm", "microohm": "Microohm", "milliohm": "Milliohm", @@ -6231,7 +6232,7 @@ "megahertz": "Megahertz", "gigahertz": "Gigahertz", "terahertz": "Terahertz", - "rpm": "Revolutions Per Minute", + "rpm": "Revolutions per minute", "candela-per-square-meter": "Candela per square meter", "candela": "Candela", "lumen": "Lumen", @@ -6243,17 +6244,17 @@ "lumens-per-watt": "Lumens per watt", "mole": "Mole", "nanomole": "Nanomole", - "micromole": "MicroMole", + "micromole": "Micromole", "millimole": "Millimole", "kilomole": "Kilomole", - "mole-per-cubic-meter": "Mole per Cubic Meter", + "mole-per-cubic-meter": "Mole per cubic meter", "rssi": "Received signal strength indicator", - "ppm": "Parts Per Million", - "ppb": "Parts Per Billion", - "micrograms-per-cubic-meter": "Micrograms per Cubic Meter", - "aqi": "AQI", + "ppm": "Parts per million", + "ppb": "Parts per billion", + "micrograms-per-cubic-meter": "Micrograms per cubic meter", + "aqi": "Aqi", "gram-per-cubic-meter": "Gram per cubic meter", - "gram-per-kilogram": "Specific Humidity", + "gram-per-kilogram": "Specific humidity", "millimeters-per-second": "Millimeters per second", "neper": "Neper", "bel": "Bel", @@ -6264,7 +6265,7 @@ "gray": "Gray", "sievert": "Sievert", "roentgen": "Roentgen", - "cps": "Counts per Second", + "cps": "Counts per second", "rad": "Rad", "rem": "Rem", "dps": "Disintegrations per second", @@ -6274,10 +6275,10 @@ "curies-per-liter": "Curies per liter", "becquerels-per-second": "Becquerels per second", "curies-per-second": "Curies per second", - "gy-per-second": "Gray per Second", - "watt-per-steradian": "Watt per Steradian", - "watt-per-square-metre-steradian": "Watt per Square Metre-Steradian", - "ph-level": "pH Level", + "gy-per-second": "Gray per second", + "watt-per-steradian": "Watt per steradian", + "watt-per-square-metre-steradian": "Watt per square metre-steradian", + "ph-level": "Ph level", "turbidity": "Turbidity", "mg-per-liter": "Milligrams per liter", "microsiemens-per-centimeter": "Microsiemens per centimeter", @@ -6303,9 +6304,9 @@ "milligrams-per-deciliter": "Milligrams per deciliter", "g-force": "G-force", "kilonewton": "Kilonewton", - "kilogram-force": "Kilogram-Force", - "pound-force": "Pound-Force", - "kilopound-force": "Kilopound-Force", + "kilogram-force": "Kilogram-force", + "pound-force": "Pound-force", + "kilopound-force": "Kilopound-force", "dyne": "Dyne", "poundal": "Poundal", "kip": "Kip", @@ -6315,7 +6316,7 @@ "atmosphere": "Atmosphere", "millibars": "Millibars", "inch-of-mercury": "One inch of mercury", - "richter-scale": "Richter Scale", + "richter-scale": "Richter scale", "nanosecond": "Nanosecond", "microsecond": "Microsecond", "millisecond": "Millisecond", @@ -6326,12 +6327,12 @@ "week": "Week", "month": "Month", "year": "Year", - "cubic-foot-per-minute": "Cubic Foot Per Minute", - "cubic-meters-per-hour": "Cubic Meters Per Hour", - "cubic-meters-per-second": "Cubic Meters Per Second", - "liter-per-second": "Liter Per Second", - "liter-per-minute": "Liter Per Minute", - "gallons-per-minute": "Gallons Per Minute", + "cubic-foot-per-minute": "Cubic foot per minute", + "cubic-meters-per-hour": "Cubic meters per hour", + "cubic-meters-per-second": "Cubic meters per second", + "liter-per-second": "Liter per second", + "liter-per-minute": "Liter per minute", + "gallons-per-minute": "Gallons per minute", "cubic-foot-per-second": "Cubic foot per second", "milliliters-per-minute": "Milliliters per minute", "cubic-decimeter-per-second": "Cubic decimeter per second", @@ -6376,7 +6377,7 @@ "megafarad": "Megafarad", "gigafarad": "Gigafarad", "terfarad": "Terfarad", - "farad-per-meter": "Farad per Meter", + "farad-per-meter": "Farad per meter", "tesla": "Tesla", "gauss": "Gauss", "kilogauss": "Kilogauss", @@ -6385,7 +6386,7 @@ "nanotesla": "Nanotesla", "kilotesla": "Kilotesla", "megatesla": "Megatesla", - "millitesla-square-meters": "millitesla square meters", + "millitesla-square-meters": "Millitesla square meters", "gamma": "Gamma", "lambda": "Lambda", "square-meter-per-second": "Square meter per second", @@ -6404,25 +6405,25 @@ "kilogram-per-meter-second": "Kilogram per meter-second", "tesla-square-meters": "Tesla square meters", "maxwell": "Maxwell", - "tesla-per-meter": "Tesla per Meter", - "gauss-per-centimeter": "Gauss per Centimeter", + "tesla-per-meter": "Tesla per meter", + "gauss-per-centimeter": "Gauss per centimeter", "weber": "Weber", "microweber": "Microweber", "milliweber": "Milliweber", - "gauss-square-centimeter": "Gauss-Square Centimeter", - "kilogauss-square-centimeter": "Kilogauss-Square Centimeter", + "gauss-square-centimeter": "Gauss-square centimeter", + "kilogauss-square-centimeter": "Kilogauss-square centimeter", "henry": "Henry", "millihenry": "Millihenry", "microhenry": "Microhenry", "nanohenry": "Nanohenry", - "henry-per-meter": "Henry per Meter", - "tesla-meter-per-ampere": "Tesla Meter per Ampere", - "gauss-per-oersted": "Gauss per Oersted", + "henry-per-meter": "Henry per meter", + "tesla-meter-per-ampere": "Tesla meter per ampere", + "gauss-per-oersted": "Gauss per oersted", "kilogram-per-mole": "Kilogram per mole", "gram-per-mole": "Gram per mole", "milligram-per-mole": "Milligram per mole", - "joule-per-mole": "Joule per Mole", - "joule-per-mole-kelvin": "Joule per Mole-Kelvin", + "joule-per-mole": "Joule per mole", + "joule-per-mole-kelvin": "Joule per mole-kelvin", "millivolts-per-meter": "Millivolts per meter", "volts-per-meter": "Volts per meter", "kilovolts-per-meter": "Kilovolts per meter", @@ -6433,7 +6434,7 @@ "rotation-per-minute": "Rotation per minute", "degrees-brix": "Degrees brix", "katal": "Katal", - "katal-per-cubic-metre": "Katal per Cubic Metre", + "katal-per-cubic-metre": "Katal per cubic metre", "paris-inch": "Paris inch" }, "user": { From bf46d974ab8b07f9a27e11277cd5758668d41e77 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 25 Jun 2025 16:11:29 +0300 Subject: [PATCH 098/435] Remove duplicated indexes --- .../main/data/upgrade/basic/schema_update.sql | 13 +++++++++++++ .../CalculatedFieldEntityMessageProcessor.java | 2 +- .../main/resources/sql/schema-entities-idx.sql | 18 ------------------ 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 2f652ce289..c959cfd6c1 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -22,3 +22,16 @@ ALTER TABLE ota_package ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id); -- UPDATE OTA PACKAGE EXTERNAL ID END + +-- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT START + +DROP INDEX IF EXISTS idx_device_external_id; +DROP INDEX IF EXISTS idx_device_profile_external_id; +DROP INDEX IF EXISTS idx_asset_external_id; +DROP INDEX IF EXISTS idx_entity_view_external_id; +DROP INDEX IF EXISTS idx_rule_chain_external_id; +DROP INDEX IF EXISTS idx_dashboard_external_id; +DROP INDEX IF EXISTS idx_customer_external_id; +DROP INDEX IF EXISTS idx_widgets_bundle_external_id; + +-- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT END diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index ebc4e60709..35539834c3 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -68,7 +68,7 @@ import java.util.stream.Collectors; */ @Slf4j public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareMsgProcessor { - // (1 for result persistence + 1 for the state persistence ) + // (1 for result persistence + 1 for the state persistence) public static final int CALLBACKS_PER_CF = 2; final TenantId tenantId; diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index 0a6d6e578d..dd37261526 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -73,26 +73,8 @@ CREATE INDEX IF NOT EXISTS idx_edge_event_id ON edge_event(id); CREATE INDEX IF NOT EXISTS idx_rpc_tenant_id_device_id ON rpc(tenant_id, device_id); -CREATE INDEX IF NOT EXISTS idx_device_external_id ON device(tenant_id, external_id); - -CREATE INDEX IF NOT EXISTS idx_device_profile_external_id ON device_profile(tenant_id, external_id); - -CREATE INDEX IF NOT EXISTS idx_asset_external_id ON asset(tenant_id, external_id); - -CREATE INDEX IF NOT EXISTS idx_entity_view_external_id ON entity_view(tenant_id, external_id); - -CREATE INDEX IF NOT EXISTS idx_rule_chain_external_id ON rule_chain(tenant_id, external_id); - -CREATE INDEX IF NOT EXISTS idx_dashboard_external_id ON dashboard(tenant_id, external_id); - -CREATE INDEX IF NOT EXISTS idx_customer_external_id ON customer(tenant_id, external_id); - -CREATE INDEX IF NOT EXISTS idx_widgets_bundle_external_id ON widgets_bundle(tenant_id, external_id); - CREATE INDEX IF NOT EXISTS idx_rule_node_external_id ON rule_node(rule_chain_id, external_id); -CREATE INDEX IF NOT EXISTS idx_ota_package_external_id ON ota_package(tenant_id, external_id); - CREATE INDEX IF NOT EXISTS idx_rule_node_type_id_configuration_version ON rule_node(type, id, configuration_version); CREATE INDEX IF NOT EXISTS idx_api_usage_state_entity_id ON api_usage_state(entity_id); From a3a5fd3180cf87b1d7ca044ec0bda2215bee30f1 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 25 Jun 2025 16:59:31 +0300 Subject: [PATCH 099/435] Small refactoring of sendErrorResponse --- .../queue/common/PartitionedQueueResponseTemplate.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java index a15fafa19a..ef1682b97f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java @@ -131,7 +131,7 @@ public class PartitionedQueueResponseTemplate Date: Thu, 26 Jun 2025 12:23:19 +0300 Subject: [PATCH 100/435] AI rule node: add Anthropic support --- application/pom.xml | 8 ++- .../Langchain4jChatModelConfigurerImpl.java | 13 ++++ .../data/ai/model/AiModelTypeIdResolver.java | 9 +++ .../data/ai/model/chat/AiChatModel.java | 4 +- .../data/ai/model/chat/AiChatModelConfig.java | 4 +- .../ai/model/chat/AnthropicChatModel.java | 60 +++++++++++++++++++ .../chat/Langchain4jChatModelConfigurer.java | 2 + .../common/data/ai/provider/AiProvider.java | 3 +- .../data/ai/provider/AiProviderConfig.java | 7 ++- .../ai/provider/AnthropicProviderConfig.java | 25 ++++++++ .../thingsboard/rule/engine/ai/TbAiNode.java | 20 +++---- 11 files changed, 135 insertions(+), 20 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.java diff --git a/application/pom.xml b/application/pom.xml index 7ec326758b..e92aeba533 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -383,11 +383,11 @@ dev.langchain4j - langchain4j-azure-open-ai + langchain4j-open-ai dev.langchain4j - langchain4j-open-ai + langchain4j-azure-open-ai dev.langchain4j @@ -401,6 +401,10 @@ dev.langchain4j langchain4j-mistral-ai + + dev.langchain4j + langchain4j-anthropic + diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index 91e9c7874e..aa4aed988d 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -25,6 +25,7 @@ import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; +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.GoogleAiGeminiChatModel; import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModel; @@ -125,6 +126,18 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .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()) + .timeout(toDuration(modelConfig.timeoutSeconds())) + .maxRetries(modelConfig.maxRetries()) + .build(); + } + private static Duration toDuration(Integer timeoutSeconds) { return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java index 34dab4b6aa..0f3634e1f4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.DatabindContext; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase; +import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModel; 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; @@ -77,6 +78,14 @@ public final class AiModelTypeIdResolver extends TypeIdResolverBase { map.put("MISTRAL_AI::ministral-3b-latest", MistralAiChatModel.class); map.put("MISTRAL_AI::open-mistral-nemo", MistralAiChatModel.class); + // Anthropic models + map.put("ANTHROPIC::claude-opus-4-0", AnthropicChatModel.class); + map.put("ANTHROPIC::claude-sonnet-4-0", AnthropicChatModel.class); + map.put("ANTHROPIC::claude-3-7-sonnet-latest", AnthropicChatModel.class); + map.put("ANTHROPIC::claude-3-5-sonnet-latest", AnthropicChatModel.class); + map.put("ANTHROPIC::claude-3-5-haiku-latest", AnthropicChatModel.class); + map.put("ANTHROPIC::claude-3-opus-latest", AnthropicChatModel.class); + typeIdToModelClass = Collections.unmodifiableMap(map); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java index 08d4630d10..2043efd860 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java @@ -20,7 +20,9 @@ import org.thingsboard.server.common.data.ai.model.AiModel; import org.thingsboard.server.common.data.ai.model.AiModelType; public sealed interface AiChatModel> extends AiModel - permits OpenAiChatModel, AzureOpenAiChatModel, GoogleAiGeminiChatModel, GoogleVertexAiGeminiChatModel, MistralAiChatModel { + permits + OpenAiChatModel, AzureOpenAiChatModel, GoogleAiGeminiChatModel, + GoogleVertexAiGeminiChatModel, MistralAiChatModel, AnthropicChatModel { ChatModel configure(Langchain4jChatModelConfigurer configurer); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java index 4ef5369005..d346b22731 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java @@ -18,7 +18,9 @@ package org.thingsboard.server.common.data.ai.model.chat; import org.thingsboard.server.common.data.ai.model.AiModelConfig; public sealed interface AiChatModelConfig> extends AiModelConfig - permits OpenAiChatModel.Config, AzureOpenAiChatModel.Config, GoogleAiGeminiChatModel.Config, GoogleVertexAiGeminiChatModel.Config, MistralAiChatModel.Config { + permits + OpenAiChatModel.Config, AzureOpenAiChatModel.Config, GoogleAiGeminiChatModel.Config, + GoogleVertexAiGeminiChatModel.Config, MistralAiChatModel.Config, AnthropicChatModel.Config { Double temperature(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java new file mode 100644 index 0000000000..329f35382a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig; + +public record AnthropicChatModel( + AnthropicProviderConfig providerConfig, + Config modelConfig +) implements AiChatModel { + + public record Config( + String modelId, + Double temperature, + Integer timeoutSeconds, + Integer maxRetries + ) implements AiChatModelConfig { + + @Override + public AnthropicChatModel.Config withTemperature(Double temperature) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + @Override + public AnthropicChatModel.Config withTimeoutSeconds(Integer timeoutSeconds) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + @Override + public AnthropicChatModel.Config withMaxRetries(Integer maxRetries) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public AnthropicChatModel withModelConfig(AnthropicChatModel.Config config) { + return new AnthropicChatModel(providerConfig, config); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java index 3602fb14a7..3cb75419f0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java @@ -29,4 +29,6 @@ public interface Langchain4jChatModelConfigurer { ChatModel configureChatModel(MistralAiChatModel chatModel); + ChatModel configureChatModel(AnthropicChatModel chatModel); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java index da4df1a076..78709fd52c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java @@ -21,6 +21,7 @@ public enum AiProvider { AZURE_OPENAI, GOOGLE_AI_GEMINI, GOOGLE_VERTEX_AI_GEMINI, - MISTRAL_AI + MISTRAL_AI, + ANTHROPIC } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java index e3b3b250cc..e13f3fac7b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java @@ -28,10 +28,13 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @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 = MistralAiProviderConfig.class, name = "MISTRAL_AI"), + @JsonSubTypes.Type(value = AnthropicProviderConfig.class, name = "ANTHROPIC") }) public sealed interface AiProviderConfig - permits OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig, GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig { + permits + OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig, + GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig, AnthropicProviderConfig { AiProvider provider(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.java new file mode 100644 index 0000000000..91ef41a072 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.java @@ -0,0 +1,25 @@ +/** + * 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.provider; + +public record AnthropicProviderConfig(String apiKey) implements AiProviderConfig { + + @Override + public AiProvider provider() { + return AiProvider.ANTHROPIC; + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 2ac3ea3e6c..edcf2065eb 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -16,7 +16,6 @@ package org.thingsboard.rule.engine.ai; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.FutureCallback; import dev.langchain4j.data.message.SystemMessage; @@ -25,7 +24,6 @@ import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.request.ResponseFormat; import dev.langchain4j.model.chat.request.ResponseFormatType; -import dev.langchain4j.model.chat.request.json.JsonSchema; import dev.langchain4j.model.chat.response.ChatResponse; import org.checkerframework.checker.nullness.qual.NonNull; import org.thingsboard.common.util.JacksonUtil; @@ -81,10 +79,13 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { throw new TbNodeException(e, true); } - responseFormat = ResponseFormat.builder() - .type(config.getResponseFormatType()) - .jsonSchema(getJsonSchema(config.getResponseFormatType(), config.getJsonSchema())) - .build(); + // LC4j AnthropicChatModel rejects requests with non-null ResponseFormat even if ResponseFormatType is TEXT + if (config.getResponseFormatType() == ResponseFormatType.JSON) { + responseFormat = ResponseFormat.builder() + .type(config.getResponseFormatType()) + .jsonSchema(config.getJsonSchema() != null ? Langchain4jJsonSchemaAdapter.fromJsonNode(config.getJsonSchema()) : null) + .build(); + } systemPrompt = config.getSystemPrompt(); userPrompt = config.getUserPrompt(); @@ -101,13 +102,6 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { } } - private static JsonSchema getJsonSchema(ResponseFormatType responseFormatType, ObjectNode jsonSchema) { - if (responseFormatType == ResponseFormatType.TEXT) { - return null; - } - return responseFormatType == ResponseFormatType.JSON && jsonSchema != null ? Langchain4jJsonSchemaAdapter.fromJsonNode(jsonSchema) : null; - } - @Override public void onMsg(TbContext ctx, TbMsg msg) { var ackedMsg = ackIfNeeded(ctx, msg); From 4f38422df8748b89f01f612292eb4ce7d34431b5 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 26 Jun 2025 12:59:41 +0300 Subject: [PATCH 101/435] AI rule node: add basic Amazon Bedrock support --- application/pom.xml | 4 ++ .../Langchain4jChatModelConfigurerImpl.java | 35 +++++++++++ .../data/ai/model/AiModelTypeIdResolver.java | 4 ++ .../data/ai/model/chat/AiChatModel.java | 3 +- .../data/ai/model/chat/AiChatModelConfig.java | 3 +- .../ai/model/chat/AmazonBedrockChatModel.java | 60 +++++++++++++++++++ .../chat/Langchain4jChatModelConfigurer.java | 2 + .../common/data/ai/provider/AiProvider.java | 3 +- .../data/ai/provider/AiProviderConfig.java | 6 +- .../provider/AmazonBedrockProviderConfig.java | 25 ++++++++ 10 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AmazonBedrockProviderConfig.java diff --git a/application/pom.xml b/application/pom.xml index e92aeba533..6a71074171 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -405,6 +405,10 @@ dev.langchain4j langchain4j-anthropic + + dev.langchain4j + langchain4j-bedrock + diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index aa4aed988d..404f9eac91 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -21,10 +21,13 @@ import com.google.cloud.vertexai.Transport; import com.google.cloud.vertexai.VertexAI; import com.google.cloud.vertexai.api.GenerationConfig; import com.google.cloud.vertexai.generativeai.GenerativeModel; +import dev.langchain4j.model.bedrock.BedrockChatModel; import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.request.ChatRequestParameters; import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; +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.GoogleAiGeminiChatModel; @@ -32,7 +35,12 @@ import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChat 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.provider.AmazonBedrockProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -138,6 +146,33 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .build(); } + @Override + public ChatModel configureChatModel(AmazonBedrockChatModel chatModel) { + AmazonBedrockProviderConfig providerConfig = chatModel.providerConfig(); + AmazonBedrockChatModel.Config modelConfig = chatModel.modelConfig(); + + var credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create(providerConfig.accessKeyId(), providerConfig.secretAccessKey()) + ); + + var bedrockClient = BedrockRuntimeClient.builder() + .region(Region.of(providerConfig.region())) + .credentialsProvider(credentialsProvider) + .build(); + + var defaultChatRequestParams = ChatRequestParameters.builder() + .temperature(modelConfig.temperature()) + .build(); + + return BedrockChatModel.builder() + .client(bedrockClient) + .modelId(modelConfig.modelId()) + .defaultRequestParameters(defaultChatRequestParams) + .timeout(toDuration(modelConfig.timeoutSeconds())) + .maxRetries(modelConfig.maxRetries()) + .build(); + } + private static Duration toDuration(Integer timeoutSeconds) { return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java index 0f3634e1f4..3c7766c4ea 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.DatabindContext; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase; +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.GoogleAiGeminiChatModel; import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModel; @@ -86,6 +87,9 @@ public final class AiModelTypeIdResolver extends TypeIdResolverBase { map.put("ANTHROPIC::claude-3-5-haiku-latest", AnthropicChatModel.class); map.put("ANTHROPIC::claude-3-opus-latest", AnthropicChatModel.class); + // Amazon Bedrock models + map.put("AMAZON_BEDROCK::amazon.nova-lite-v1:0", AmazonBedrockChatModel.class); + typeIdToModelClass = Collections.unmodifiableMap(map); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java index 2043efd860..ac0f2226f9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java @@ -22,7 +22,8 @@ import org.thingsboard.server.common.data.ai.model.AiModelType; public sealed interface AiChatModel> extends AiModel permits OpenAiChatModel, AzureOpenAiChatModel, GoogleAiGeminiChatModel, - GoogleVertexAiGeminiChatModel, MistralAiChatModel, AnthropicChatModel { + GoogleVertexAiGeminiChatModel, MistralAiChatModel, AnthropicChatModel, + AmazonBedrockChatModel { ChatModel configure(Langchain4jChatModelConfigurer configurer); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java index d346b22731..e0d7232fa6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java @@ -20,7 +20,8 @@ import org.thingsboard.server.common.data.ai.model.AiModelConfig; public sealed interface AiChatModelConfig> extends AiModelConfig permits OpenAiChatModel.Config, AzureOpenAiChatModel.Config, GoogleAiGeminiChatModel.Config, - GoogleVertexAiGeminiChatModel.Config, MistralAiChatModel.Config, AnthropicChatModel.Config { + GoogleVertexAiGeminiChatModel.Config, MistralAiChatModel.Config, AnthropicChatModel.Config, + AmazonBedrockChatModel.Config { Double temperature(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java new file mode 100644 index 0000000000..e3cbaf426c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; + +public record AmazonBedrockChatModel( + AmazonBedrockProviderConfig providerConfig, + Config modelConfig +) implements AiChatModel { + + public record Config( + String modelId, + Double temperature, + Integer timeoutSeconds, + Integer maxRetries + ) implements AiChatModelConfig { + + @Override + public AmazonBedrockChatModel.Config withTemperature(Double temperature) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + @Override + public AmazonBedrockChatModel.Config withTimeoutSeconds(Integer timeoutSeconds) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + @Override + public AmazonBedrockChatModel.Config withMaxRetries(Integer maxRetries) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public AmazonBedrockChatModel withModelConfig(AmazonBedrockChatModel.Config config) { + return new AmazonBedrockChatModel(providerConfig, config); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java index 3cb75419f0..d9d4c1dbb6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java @@ -31,4 +31,6 @@ public interface Langchain4jChatModelConfigurer { ChatModel configureChatModel(AnthropicChatModel chatModel); + ChatModel configureChatModel(AmazonBedrockChatModel chatModel); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java index 78709fd52c..55b3cbe7f4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java @@ -22,6 +22,7 @@ public enum AiProvider { GOOGLE_AI_GEMINI, GOOGLE_VERTEX_AI_GEMINI, MISTRAL_AI, - ANTHROPIC + ANTHROPIC, + AMAZON_BEDROCK } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java index e13f3fac7b..890ea197a0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java @@ -29,12 +29,14 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @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 = AnthropicProviderConfig.class, name = "ANTHROPIC"), + @JsonSubTypes.Type(value = AmazonBedrockProviderConfig.class, name = "AMAZON_BEDROCK") }) public sealed interface AiProviderConfig permits OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig, - GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig, AnthropicProviderConfig { + GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig, AnthropicProviderConfig, + AmazonBedrockProviderConfig { AiProvider provider(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AmazonBedrockProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AmazonBedrockProviderConfig.java new file mode 100644 index 0000000000..7ecb0bcdb4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AmazonBedrockProviderConfig.java @@ -0,0 +1,25 @@ +/** + * 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.provider; + +public record AmazonBedrockProviderConfig(String region, String accessKeyId, String secretAccessKey) implements AiProviderConfig { + + @Override + public AiProvider provider() { + return AiProvider.AMAZON_BEDROCK; + } + +} From 4507b312decda4f8756ce9385a2bb79192bf5be1 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 26 Jun 2025 15:02:05 +0300 Subject: [PATCH 102/435] AI rule node: add support for GitHub Models --- application/pom.xml | 4 ++ .../Langchain4jChatModelConfigurerImpl.java | 17 +++++- .../data/ai/model/AiModelTypeIdResolver.java | 4 ++ .../data/ai/model/chat/AiChatModel.java | 2 +- .../data/ai/model/chat/AiChatModelConfig.java | 2 +- .../ai/model/chat/GitHubModelsChatModel.java | 60 +++++++++++++++++++ .../chat/Langchain4jChatModelConfigurer.java | 2 + .../common/data/ai/provider/AiProvider.java | 3 +- .../data/ai/provider/AiProviderConfig.java | 5 +- .../provider/GithubModelsProviderConfig.java | 25 ++++++++ 10 files changed, 116 insertions(+), 8 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GithubModelsProviderConfig.java diff --git a/application/pom.xml b/application/pom.xml index 6a71074171..543724fa10 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -409,6 +409,10 @@ dev.langchain4j langchain4j-bedrock + + dev.langchain4j + langchain4j-github-models + diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index 404f9eac91..72e9506fdc 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.ai; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.auth.oauth2.ServiceAccountCredentials; import com.google.cloud.vertexai.Transport; import com.google.cloud.vertexai.VertexAI; @@ -30,6 +29,7 @@ import org.thingsboard.common.util.JacksonUtil; 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.Langchain4jChatModelConfigurer; @@ -91,11 +91,10 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur GoogleVertexAiGeminiChatModel.Config modelConfig = chatModel.modelConfig(); // construct service account credentials using service account key JSON - ObjectNode serviceAccountKeyJson = providerConfig.serviceAccountKey(); ServiceAccountCredentials serviceAccountCredentials; try { serviceAccountCredentials = ServiceAccountCredentials - .fromStream(new ByteArrayInputStream(JacksonUtil.writeValueAsBytes(serviceAccountKeyJson))); + .fromStream(new ByteArrayInputStream(JacksonUtil.writeValueAsBytes(providerConfig.serviceAccountKey()))); } catch (IOException e) { throw new RuntimeException("Failed to parse service account key JSON", e); } @@ -173,6 +172,18 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .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()) + .timeout(toDuration(modelConfig.timeoutSeconds())) + .maxRetries(modelConfig.maxRetries()) + .build(); + } + private static Duration toDuration(Integer timeoutSeconds) { return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java index 3c7766c4ea..d4339ccd7b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase; 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.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; @@ -90,6 +91,9 @@ public final class AiModelTypeIdResolver extends TypeIdResolverBase { // Amazon Bedrock models map.put("AMAZON_BEDROCK::amazon.nova-lite-v1:0", AmazonBedrockChatModel.class); + // GitHub Models models + map.put("GITHUB_MODELS::gpt-4o", GitHubModelsChatModel.class); + typeIdToModelClass = Collections.unmodifiableMap(map); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java index ac0f2226f9..b9a2737b20 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java @@ -23,7 +23,7 @@ public sealed interface AiChatModel> extends AiMo permits OpenAiChatModel, AzureOpenAiChatModel, GoogleAiGeminiChatModel, GoogleVertexAiGeminiChatModel, MistralAiChatModel, AnthropicChatModel, - AmazonBedrockChatModel { + AmazonBedrockChatModel, GitHubModelsChatModel { ChatModel configure(Langchain4jChatModelConfigurer configurer); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java index e0d7232fa6..d5e794cec0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java @@ -21,7 +21,7 @@ public sealed interface AiChatModelConfig> extend permits OpenAiChatModel.Config, AzureOpenAiChatModel.Config, GoogleAiGeminiChatModel.Config, GoogleVertexAiGeminiChatModel.Config, MistralAiChatModel.Config, AnthropicChatModel.Config, - AmazonBedrockChatModel.Config { + AmazonBedrockChatModel.Config, GitHubModelsChatModel.Config { Double temperature(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java new file mode 100644 index 0000000000..165873e6e3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.provider.GithubModelsProviderConfig; + +public record GitHubModelsChatModel( + GithubModelsProviderConfig providerConfig, + Config modelConfig +) implements AiChatModel { + + public record Config( + String modelId, + Double temperature, + Integer timeoutSeconds, + Integer maxRetries + ) implements AiChatModelConfig { + + @Override + public GitHubModelsChatModel.Config withTemperature(Double temperature) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + @Override + public GitHubModelsChatModel.Config withTimeoutSeconds(Integer timeoutSeconds) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + @Override + public GitHubModelsChatModel.Config withMaxRetries(Integer maxRetries) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public GitHubModelsChatModel withModelConfig(GitHubModelsChatModel.Config config) { + return new GitHubModelsChatModel(providerConfig, config); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java index d9d4c1dbb6..ea9fb80e73 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java @@ -33,4 +33,6 @@ public interface Langchain4jChatModelConfigurer { ChatModel configureChatModel(AmazonBedrockChatModel chatModel); + ChatModel configureChatModel(GitHubModelsChatModel chatModel); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java index 55b3cbe7f4..d0a5bd0510 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java @@ -23,6 +23,7 @@ public enum AiProvider { GOOGLE_VERTEX_AI_GEMINI, MISTRAL_AI, ANTHROPIC, - AMAZON_BEDROCK + AMAZON_BEDROCK, + GITHUB_MODELS } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java index 890ea197a0..04ca775f7f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java @@ -30,13 +30,14 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @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 = AmazonBedrockProviderConfig.class, name = "AMAZON_BEDROCK"), + @JsonSubTypes.Type(value = GithubModelsProviderConfig.class, name = "GITHUB_MODELS") }) public sealed interface AiProviderConfig permits OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig, GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig, AnthropicProviderConfig, - AmazonBedrockProviderConfig { + AmazonBedrockProviderConfig, GithubModelsProviderConfig { AiProvider provider(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GithubModelsProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GithubModelsProviderConfig.java new file mode 100644 index 0000000000..afbac79dd1 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GithubModelsProviderConfig.java @@ -0,0 +1,25 @@ +/** + * 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.provider; + +public record GithubModelsProviderConfig(String personalAccessToken) implements AiProviderConfig { + + @Override + public AiProvider provider() { + return AiProvider.GITHUB_MODELS; + } + +} From 15eb14409d826d27061d14d154830f37b5e831f2 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 26 Jun 2025 15:49:46 +0300 Subject: [PATCH 103/435] UI: Fixed support unit conversion in range chart widgets --- .../lib/chart/range-chart-widget.component.ts | 15 +++++-------- .../lib/chart/range-chart-widget.models.ts | 22 +++++++++---------- .../lib/chart/time-series-chart.models.ts | 4 ++-- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts index 1d1257c2e8..c765a65b80 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts @@ -49,7 +49,7 @@ import { ImagePipe } from '@shared/pipe/image.pipe'; import { DomSanitizer } from '@angular/platform-browser'; import { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart'; import { WidgetComponent } from '@home/components/widget/widget.component'; -import { TbUnitConverter } from '@shared/models/unit.models'; +import { TbUnit } from '@shared/models/unit.models'; import { UnitService } from '@core/services/unit.service'; @Component({ @@ -80,8 +80,7 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn visibleRangeItems: RangeItem[]; private decimals = 0; - private units: string = ''; - private unitConvertor: TbUnitConverter; + private units: TbUnit = ''; private rangeItems: RangeItem[]; @@ -100,22 +99,20 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn const unitService = this.ctx.$injector.get(UnitService); this.decimals = this.ctx.decimals; - let units = this.ctx.units; + this.units = this.ctx.units; const dataKey = getDataKey(this.ctx.datasources); if (isDefinedAndNotNull(dataKey?.decimals)) { this.decimals = dataKey.decimals; } if (dataKey?.units) { - units = dataKey.units; + this.units = dataKey.units; } if (dataKey) { dataKey.settings = rangeChartTimeSeriesKeySettings(this.settings); } - this.units = unitService.getTargetUnitSymbol(units); - this.unitConvertor = unitService.geUnitConverter(units); const valueFormat = ValueFormatProcessor.fromSettings(this.ctx.$injector, { - units, + units: this.units, decimals: this.decimals, ignoreUnitSymbol: true }); @@ -138,7 +135,7 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn } ngAfterViewInit() { - const settings = rangeChartTimeSeriesSettings(this.settings, this.rangeItems, this.decimals, this.units, this.unitConvertor); + const settings = rangeChartTimeSeriesSettings(this.settings, this.rangeItems, this.decimals, this.units); this.timeSeriesChart = new TbTimeSeriesChart(this.ctx, settings, this.chartShape.nativeElement, this.renderer); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts index f4210e2203..de59cc7fa3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts @@ -57,6 +57,7 @@ import { import { TimeSeriesChartTooltipWidgetSettings } from '@home/components/widget/lib/chart/time-series-chart-tooltip.models'; +import { TbUnit } from '@shared/models/unit.models'; export interface RangeItem { index: number; @@ -221,13 +222,13 @@ export const rangeChartDefaultSettings: RangeChartWidgetSettings = { }; export const rangeChartTimeSeriesSettings = (settings: RangeChartWidgetSettings, rangeItems: RangeItem[], - decimals: number, units: string, valueConvertor: (x: number) => number): DeepPartial => { + decimals: number, units: TbUnit): DeepPartial => { let thresholds: DeepPartial[] = settings.showRangeThresholds ? getMarkPoints(rangeItems).map(item => ({ ...{type: ValueSourceType.constant, yAxisId: 'default', units, decimals, - value: valueConvertor(item)}, + value: item}, ...settings.rangeThreshold } as DeepPartial)) : []; if (settings.thresholds?.length) { @@ -240,10 +241,8 @@ export const rangeChartTimeSeriesSettings = (settings: RangeChartWidgetSettings, yAxes: { default: { ...settings.yAxis, - ...{ - decimals, - units - } + decimals, + units } }, xAxis: settings.xAxis, @@ -299,14 +298,15 @@ export const toRangeItems = (colorRanges: Array, valueFormat: ValueF for (let i = 0; i < ranges.length; i++) { const range = ranges[i]; let from = range.from; - const to = isDefinedAndNotNull(range.to) ? Number(valueFormat.format(range.to)) : range.to; + const to = range.to; if (i > 0) { const prevRange = ranges[i - 1]; if (isNumber(prevRange.to) && isNumber(from) && from < prevRange.to) { from = prevRange.to; } } - from = isDefinedAndNotNull(from) ? Number(valueFormat.format(from)) : from; + const formatToValue = isDefinedAndNotNull(to) ? Number(valueFormat.format(to)) : to; + const formatFromValue = isDefinedAndNotNull(from) ? Number(valueFormat.format(from)) : from; rangeItems.push( { index: counter++, @@ -315,12 +315,12 @@ export const toRangeItems = (colorRanges: Array, valueFormat: ValueF visible: true, from, to, - label: rangeItemLabel(from, to), - piece: createTimeSeriesChartVisualMapPiece(range.color, from, to) + label: rangeItemLabel(formatFromValue, formatToValue), + piece: createTimeSeriesChartVisualMapPiece(range.color, formatFromValue, formatToValue) } ); if (!isNumber(from) || !isNumber(to)) { - const value = !isNumber(from) ? to : from; + const value = !isNumber(from) ? formatToValue : formatFromValue; rangeItems.push( { index: counter++, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index b29f809fef..86d952f2e2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -98,7 +98,7 @@ import { TimeSeriesChartTooltipValueFormatFunction, TimeSeriesChartTooltipWidgetSettings } from '@home/components/widget/lib/chart/time-series-chart-tooltip.models'; -import { TbUnitConverter } from '@shared/models/unit.models'; +import { TbUnit, TbUnitConverter } from '@shared/models/unit.models'; type TimeSeriesChartDataEntry = [number, any, number, number]; @@ -377,7 +377,7 @@ export type TimeSeriesChartTicksFormatter = export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSettings { id?: TimeSeriesChartYAxisId; order?: number; - units?: string; + units?: TbUnit; decimals?: number; interval?: number; splitNumber?: number; From 2aa675e108ff97f52389712f2f6bb141073c33a8 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 26 Jun 2025 15:50:10 +0300 Subject: [PATCH 104/435] UI: Add new unit m/min --- ui-ngx/src/app/shared/models/units/speed.ts | 7 ++++++- ui-ngx/src/assets/locale/locale.constant-en_US.json | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/models/units/speed.ts b/ui-ngx/src/app/shared/models/units/speed.ts index 3e3bad7b03..78433c60ae 100644 --- a/ui-ngx/src/app/shared/models/units/speed.ts +++ b/ui-ngx/src/app/shared/models/units/speed.ts @@ -18,7 +18,7 @@ import { TbMeasure, TbMeasureUnits } from '@shared/models/unit.models'; export type SpeedUnits = SpeedMetricUnits | SpeedImperialUnits; -export type SpeedMetricUnits = 'm/s' | 'km/h' | 'mm/min' | 'mm/s'; +export type SpeedMetricUnits = 'm/s' | 'km/h' | 'mm/min' | 'm/min' | 'mm/s'; export type SpeedImperialUnits = 'mph' | 'kt' | 'ft/s' | 'ft/min' | 'in/s' | 'in/h'; const METRIC: TbMeasureUnits = { @@ -37,6 +37,11 @@ const METRIC: TbMeasureUnits = { 'mm/min': { name: 'unit.millimeters-per-minute', tags: ['feed rate', 'cutting feed rate'], + to_anchor: 0.00006, + }, + 'm/min': { + name: 'unit.meter-per-minute', + tags: ['velocity', 'pace'], to_anchor: 0.06, }, 'mm/s': { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index fe07c52ea8..2c0fb7b617 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6083,6 +6083,7 @@ "inch-per-second": "Inch per second", "inch-per-hour": "Inch per hour", "millimeters-per-minute": "Millimeters per minute", + "meter-per-minute": "Meter per minute", "kilometer-per-hour-squared": "Kilometer per hour squared", "foot-per-second-squared": "Foot per second squared", "pascal": "Pascal", From f2c6fd45d85e24a9246193972623cf49c827fd5a Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 26 Jun 2025 17:46:55 +0300 Subject: [PATCH 105/435] AI rule node: serialize model type to JSON --- .../org/thingsboard/server/common/data/ai/model/AiModel.java | 2 ++ .../common/data/ai/model/chat/AmazonBedrockChatModel.java | 4 +++- .../server/common/data/ai/model/chat/AnthropicChatModel.java | 4 +++- .../common/data/ai/model/chat/AzureOpenAiChatModel.java | 4 +++- .../common/data/ai/model/chat/GitHubModelsChatModel.java | 4 +++- .../common/data/ai/model/chat/GoogleAiGeminiChatModel.java | 4 +++- .../data/ai/model/chat/GoogleVertexAiGeminiChatModel.java | 4 +++- .../server/common/data/ai/model/chat/MistralAiChatModel.java | 4 +++- .../server/common/data/ai/model/chat/OpenAiChatModel.java | 4 +++- 9 files changed, 26 insertions(+), 8 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java index 114fe2a528..f8e3eb6853 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.ai.model; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; @@ -29,6 +30,7 @@ public interface AiModel> { AiProviderConfig providerConfig(); + @JsonProperty("modelType") AiModelType modelType(); C modelConfig(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java index e3cbaf426c..26eb885078 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java @@ -16,9 +16,11 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; public record AmazonBedrockChatModel( + AiModelType modelType, AmazonBedrockProviderConfig providerConfig, Config modelConfig ) implements AiChatModel { @@ -54,7 +56,7 @@ public record AmazonBedrockChatModel( @Override public AmazonBedrockChatModel withModelConfig(AmazonBedrockChatModel.Config config) { - return new AmazonBedrockChatModel(providerConfig, config); + return new AmazonBedrockChatModel(modelType, providerConfig, config); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java index 329f35382a..93c195ecab 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java @@ -16,9 +16,11 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig; public record AnthropicChatModel( + AiModelType modelType, AnthropicProviderConfig providerConfig, Config modelConfig ) implements AiChatModel { @@ -54,7 +56,7 @@ public record AnthropicChatModel( @Override public AnthropicChatModel withModelConfig(AnthropicChatModel.Config config) { - return new AnthropicChatModel(providerConfig, config); + return new AnthropicChatModel(modelType, providerConfig, config); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java index 9920b546f0..70e8b26711 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java @@ -16,9 +16,11 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; public record AzureOpenAiChatModel( + AiModelType modelType, AzureOpenAiProviderConfig providerConfig, Config modelConfig ) implements AiChatModel { @@ -54,7 +56,7 @@ public record AzureOpenAiChatModel( @Override public AzureOpenAiChatModel withModelConfig(AzureOpenAiChatModel.Config config) { - return new AzureOpenAiChatModel(providerConfig, config); + return new AzureOpenAiChatModel(modelType, providerConfig, config); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java index 165873e6e3..828169ec37 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java @@ -16,9 +16,11 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.GithubModelsProviderConfig; public record GitHubModelsChatModel( + AiModelType modelType, GithubModelsProviderConfig providerConfig, Config modelConfig ) implements AiChatModel { @@ -54,7 +56,7 @@ public record GitHubModelsChatModel( @Override public GitHubModelsChatModel withModelConfig(GitHubModelsChatModel.Config config) { - return new GitHubModelsChatModel(providerConfig, config); + return new GitHubModelsChatModel(modelType, providerConfig, config); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java index c09903b305..80b7e46858 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java @@ -16,9 +16,11 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; public record GoogleAiGeminiChatModel( + AiModelType modelType, GoogleAiGeminiProviderConfig providerConfig, Config modelConfig ) implements AiChatModel { @@ -54,7 +56,7 @@ public record GoogleAiGeminiChatModel( @Override public GoogleAiGeminiChatModel withModelConfig(GoogleAiGeminiChatModel.Config config) { - return new GoogleAiGeminiChatModel(providerConfig, config); + return new GoogleAiGeminiChatModel(modelType, providerConfig, config); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java index a340430828..201311de23 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java @@ -16,9 +16,11 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; public record GoogleVertexAiGeminiChatModel( + AiModelType modelType, GoogleVertexAiGeminiProviderConfig providerConfig, Config modelConfig ) implements AiChatModel { @@ -54,7 +56,7 @@ public record GoogleVertexAiGeminiChatModel( @Override public GoogleVertexAiGeminiChatModel withModelConfig(GoogleVertexAiGeminiChatModel.Config config) { - return new GoogleVertexAiGeminiChatModel(providerConfig, config); + return new GoogleVertexAiGeminiChatModel(modelType, providerConfig, config); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java index e4eae1b766..493c0582c6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java @@ -16,9 +16,11 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; public record MistralAiChatModel( + AiModelType modelType, MistralAiProviderConfig providerConfig, Config modelConfig ) implements AiChatModel { @@ -54,7 +56,7 @@ public record MistralAiChatModel( @Override public MistralAiChatModel withModelConfig(Config config) { - return new MistralAiChatModel(providerConfig, config); + return new MistralAiChatModel(modelType, providerConfig, config); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java index a4d7401cbf..3c138d97b7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java @@ -16,9 +16,11 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; public record OpenAiChatModel( + AiModelType modelType, OpenAiProviderConfig providerConfig, Config modelConfig ) implements AiChatModel { @@ -54,7 +56,7 @@ public record OpenAiChatModel( @Override public OpenAiChatModel withModelConfig(OpenAiChatModel.Config config) { - return new OpenAiChatModel(providerConfig, config); + return new OpenAiChatModel(modelType, providerConfig, config); } } From 00a653c29fc887889dcbe3c5c07e9e8c4a1e2198 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 27 Jun 2025 14:56:48 +0300 Subject: [PATCH 106/435] AI rule node: use `Map.ofEntries()` in model type resolver --- .../data/ai/model/AiModelTypeIdResolver.java | 124 ++++++++---------- 1 file changed, 58 insertions(+), 66 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java index d4339ccd7b..ff31515096 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java @@ -27,75 +27,67 @@ import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChat import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModel; import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModel; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; public final class AiModelTypeIdResolver extends TypeIdResolverBase { - private static final Map> typeIdToModelClass; - - static { - Map> map = new HashMap<>(); - - // OpenAI models - map.put("OPENAI::o4-mini", OpenAiChatModel.class); - // map.put("OPENAI::o3-pro", OpenAiChatModel.class); // needs verification with Gov ID :) - // map.put("OPENAI::o3", OpenAiChatModel.class); // needs verification with Gov ID :) - map.put("OPENAI::o3-mini", OpenAiChatModel.class); - // map.put("OPENAI::o1-pro", OpenAiChatModel.class); // LC4j sends requests to v1/chat/completions, but o1-pro is only supported in v1/responses - map.put("OPENAI::o1", OpenAiChatModel.class); - map.put("OPENAI::gpt-4.1", OpenAiChatModel.class); - map.put("OPENAI::gpt-4.1-mini", OpenAiChatModel.class); - map.put("OPENAI::gpt-4.1-nano", OpenAiChatModel.class); - map.put("OPENAI::gpt-4o", OpenAiChatModel.class); - map.put("OPENAI::gpt-4o-mini", OpenAiChatModel.class); - - // Google AI Gemini models - map.put("GOOGLE_AI_GEMINI::gemini-2.5-pro", GoogleAiGeminiChatModel.class); - map.put("GOOGLE_AI_GEMINI::gemini-2.5-flash", GoogleAiGeminiChatModel.class); - map.put("GOOGLE_AI_GEMINI::gemini-2.0-flash", GoogleAiGeminiChatModel.class); - map.put("GOOGLE_AI_GEMINI::gemini-2.0-flash-lite", GoogleAiGeminiChatModel.class); - map.put("GOOGLE_AI_GEMINI::gemini-1.5-pro", GoogleAiGeminiChatModel.class); - map.put("GOOGLE_AI_GEMINI::gemini-1.5-flash", GoogleAiGeminiChatModel.class); - map.put("GOOGLE_AI_GEMINI::gemini-1.5-flash-8b", GoogleAiGeminiChatModel.class); - - // Google Vertex AI Gemini models - map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-2.5-pro", GoogleVertexAiGeminiChatModel.class); - map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-2.5-flash", GoogleVertexAiGeminiChatModel.class); - map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-2.0-flash", GoogleVertexAiGeminiChatModel.class); - map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-2.0-flash-lite", GoogleVertexAiGeminiChatModel.class); - map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-1.5-pro", GoogleVertexAiGeminiChatModel.class); - map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-1.5-flash", GoogleVertexAiGeminiChatModel.class); - map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-1.5-flash-8b", GoogleVertexAiGeminiChatModel.class); - - // Mistral AI models - map.put("MISTRAL_AI::magistral-medium-latest", MistralAiChatModel.class); - map.put("MISTRAL_AI::magistral-small-latest", MistralAiChatModel.class); - map.put("MISTRAL_AI::mistral-large-latest", MistralAiChatModel.class); - map.put("MISTRAL_AI::mistral-medium-latest", MistralAiChatModel.class); - map.put("MISTRAL_AI::mistral-small-latest", MistralAiChatModel.class); - map.put("MISTRAL_AI::pixtral-large-latest", MistralAiChatModel.class); - map.put("MISTRAL_AI::ministral-8b-latest", MistralAiChatModel.class); - map.put("MISTRAL_AI::ministral-3b-latest", MistralAiChatModel.class); - map.put("MISTRAL_AI::open-mistral-nemo", MistralAiChatModel.class); - - // Anthropic models - map.put("ANTHROPIC::claude-opus-4-0", AnthropicChatModel.class); - map.put("ANTHROPIC::claude-sonnet-4-0", AnthropicChatModel.class); - map.put("ANTHROPIC::claude-3-7-sonnet-latest", AnthropicChatModel.class); - map.put("ANTHROPIC::claude-3-5-sonnet-latest", AnthropicChatModel.class); - map.put("ANTHROPIC::claude-3-5-haiku-latest", AnthropicChatModel.class); - map.put("ANTHROPIC::claude-3-opus-latest", AnthropicChatModel.class); - - // Amazon Bedrock models - map.put("AMAZON_BEDROCK::amazon.nova-lite-v1:0", AmazonBedrockChatModel.class); - - // GitHub Models models - map.put("GITHUB_MODELS::gpt-4o", GitHubModelsChatModel.class); - - typeIdToModelClass = Collections.unmodifiableMap(map); - } + private static final Map> typeIdToModelClass = Map.ofEntries( + // OpenAI models + Map.entry("OPENAI::o4-mini", OpenAiChatModel.class), + // Map.entry("OPENAI::o3-pro", OpenAiChatModel.class); // needs verification with Gov ID :) + // Map.entry("OPENAI::o3", OpenAiChatModel.class); // needs verification with Gov ID :) + Map.entry("OPENAI::o3-mini", OpenAiChatModel.class), + // Map.entry("OPENAI::o1-pro", OpenAiChatModel.class); // LC4j sends requests to v1/chat/completions, but o1-pro is only supported in v1/responses + Map.entry("OPENAI::o1", OpenAiChatModel.class), + Map.entry("OPENAI::gpt-4.1", OpenAiChatModel.class), + Map.entry("OPENAI::gpt-4.1-mini", OpenAiChatModel.class), + Map.entry("OPENAI::gpt-4.1-nano", OpenAiChatModel.class), + Map.entry("OPENAI::gpt-4o", OpenAiChatModel.class), + Map.entry("OPENAI::gpt-4o-mini", OpenAiChatModel.class), + + // Google AI Gemini models + Map.entry("GOOGLE_AI_GEMINI::gemini-2.5-pro", GoogleAiGeminiChatModel.class), + Map.entry("GOOGLE_AI_GEMINI::gemini-2.5-flash", GoogleAiGeminiChatModel.class), + Map.entry("GOOGLE_AI_GEMINI::gemini-2.0-flash", GoogleAiGeminiChatModel.class), + Map.entry("GOOGLE_AI_GEMINI::gemini-2.0-flash-lite", GoogleAiGeminiChatModel.class), + Map.entry("GOOGLE_AI_GEMINI::gemini-1.5-pro", GoogleAiGeminiChatModel.class), + Map.entry("GOOGLE_AI_GEMINI::gemini-1.5-flash", GoogleAiGeminiChatModel.class), + Map.entry("GOOGLE_AI_GEMINI::gemini-1.5-flash-8b", GoogleAiGeminiChatModel.class), + + // Google Vertex AI Gemini models + Map.entry("GOOGLE_VERTEX_AI_GEMINI::gemini-2.5-pro", GoogleVertexAiGeminiChatModel.class), + Map.entry("GOOGLE_VERTEX_AI_GEMINI::gemini-2.5-flash", GoogleVertexAiGeminiChatModel.class), + Map.entry("GOOGLE_VERTEX_AI_GEMINI::gemini-2.0-flash", GoogleVertexAiGeminiChatModel.class), + Map.entry("GOOGLE_VERTEX_AI_GEMINI::gemini-2.0-flash-lite", GoogleVertexAiGeminiChatModel.class), + Map.entry("GOOGLE_VERTEX_AI_GEMINI::gemini-1.5-pro", GoogleVertexAiGeminiChatModel.class), + Map.entry("GOOGLE_VERTEX_AI_GEMINI::gemini-1.5-flash", GoogleVertexAiGeminiChatModel.class), + Map.entry("GOOGLE_VERTEX_AI_GEMINI::gemini-1.5-flash-8b", GoogleVertexAiGeminiChatModel.class), + + // Mistral AI models + Map.entry("MISTRAL_AI::magistral-medium-latest", MistralAiChatModel.class), + Map.entry("MISTRAL_AI::magistral-small-latest", MistralAiChatModel.class), + Map.entry("MISTRAL_AI::mistral-large-latest", MistralAiChatModel.class), + Map.entry("MISTRAL_AI::mistral-medium-latest", MistralAiChatModel.class), + Map.entry("MISTRAL_AI::mistral-small-latest", MistralAiChatModel.class), + Map.entry("MISTRAL_AI::pixtral-large-latest", MistralAiChatModel.class), + Map.entry("MISTRAL_AI::ministral-8b-latest", MistralAiChatModel.class), + Map.entry("MISTRAL_AI::ministral-3b-latest", MistralAiChatModel.class), + Map.entry("MISTRAL_AI::open-mistral-nemo", MistralAiChatModel.class), + + // Anthropic models + Map.entry("ANTHROPIC::claude-opus-4-0", AnthropicChatModel.class), + Map.entry("ANTHROPIC::claude-sonnet-4-0", AnthropicChatModel.class), + Map.entry("ANTHROPIC::claude-3-7-sonnet-latest", AnthropicChatModel.class), + Map.entry("ANTHROPIC::claude-3-5-sonnet-latest", AnthropicChatModel.class), + Map.entry("ANTHROPIC::claude-3-5-haiku-latest", AnthropicChatModel.class), + Map.entry("ANTHROPIC::claude-3-opus-latest", AnthropicChatModel.class), + + // Amazon Bedrock models + Map.entry("AMAZON_BEDROCK::amazon.nova-lite-v1:0", AmazonBedrockChatModel.class), + + // GitHub Models models + Map.entry("GITHUB_MODELS::gpt-4o", GitHubModelsChatModel.class) + ); private JavaType baseType; @@ -117,7 +109,7 @@ public final class AiModelTypeIdResolver extends TypeIdResolverBase { @Override public JavaType typeFromId(DatabindContext context, String id) { Class modelClass = typeIdToModelClass.get(id); - if (modelClass == null) { + if (modelClass == null) { // TODO: if provider is unknown - throw, if provider is valid but model is unknown - fallback to default model throw new IllegalArgumentException("Unknown model type ID: " + id); } return context.constructSpecializedType(baseType, modelClass); From d732e31370b1ea39886b3d698d57a103ddae1a8f Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 27 Jun 2025 14:58:08 +0300 Subject: [PATCH 107/435] AI rule node: support sorting on provider and model ID --- .../controller/AiModelSettingsController.java | 26 +++------ .../org/thingsboard/server/dao/DaoUtil.java | 23 ++++---- .../dao/ai/AiModelSettingsServiceImpl.java | 3 +- .../dao/model/sql/AiModelSettingsEntity.java | 12 +++- .../server/dao/service/Validator.java | 56 ++++++++++++++++--- .../dao/sql/ai/JpaAiModelSettingsDao.java | 24 ++++++-- 6 files changed, 101 insertions(+), 43 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java index 69c3181e26..9659be1941 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java @@ -35,7 +35,6 @@ import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.Optional; -import java.util.Set; import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.AI_MODEL_SETTINGS_TEXT_SEARCH_DESCRIPTION; @@ -48,9 +47,7 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHO @RestController @RequestMapping("/api/ai-model-settings") -public class AiModelSettingsController extends BaseController { - - private static final Set ALLOWED_SORT_PROPERTIES = Set.of("createdTime", "name"); +class AiModelSettingsController extends BaseController { @ApiOperation( value = "Create or update AI model settings (saveAiModelSettings)", @@ -75,16 +72,16 @@ public class AiModelSettingsController extends BaseController { TENANT_AUTHORITY_PARAGRAPH ) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping("/{aiModelSettingsId}") + @GetMapping("/{settingsUuid}") public AiModelSettings getAiModelSettingsById( @Parameter( description = "ID of the AI model settings record", required = true, example = "de7900d4-30e2-11f0-9cd2-0242ac120002" ) - @PathVariable("aiModelSettingsId") UUID aiModelSettingsUuid + @PathVariable UUID settingsUuid ) throws ThingsboardException { - return checkAiModelSettingsId(new AiModelSettingsId(aiModelSettingsUuid), Operation.READ); + return checkAiModelSettingsId(new AiModelSettingsId(settingsUuid), Operation.READ); } @ApiOperation( @@ -101,24 +98,17 @@ public class AiModelSettingsController extends BaseController { @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"})) + @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); - validateSortProperty(sortProperty); var pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return aiModelSettingsService.findAiModelSettingsByTenantId(user.getTenantId(), pageLink); } - private static void validateSortProperty(String sortProperty) { - if (sortProperty != null && !ALLOWED_SORT_PROPERTIES.contains(sortProperty)) { - throw new IllegalArgumentException("Unsupported sort property '" + sortProperty + "'! Only '" + String.join("', '", ALLOWED_SORT_PROPERTIES) + "' are allowed."); - } - } - @ApiOperation( value = "Delete AI model settings by ID (deleteAiModelSettingsById)", notes = "Deletes the AI model settings record by its `id`. " + @@ -127,17 +117,17 @@ public class AiModelSettingsController extends BaseController { TENANT_AUTHORITY_PARAGRAPH ) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @DeleteMapping("/{aiModelSettingsId}") + @DeleteMapping("/{settingsUuid}") public boolean deleteAiModelSettingsById( @Parameter( description = "ID of the AI model settings record", required = true, example = "de7900d4-30e2-11f0-9cd2-0242ac120002" ) - @PathVariable("aiModelSettingsId") UUID aiModelSettingsUuid + @PathVariable UUID settingsUuid ) throws ThingsboardException { var user = getCurrentUser(); - var settingsId = new AiModelSettingsId(aiModelSettingsUuid); + var settingsId = new AiModelSettingsId(settingsUuid); accessControlService.checkPermission(user, Resource.AI_MODEL_SETTINGS, Operation.DELETE); Optional toDelete = aiModelSettingsService.findAiModelSettingsByTenantIdAndId(user.getTenantId(), settingsId); if (toDelete.isEmpty()) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index 43b651bb24..7b158fe9a0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -43,10 +43,9 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; -public abstract class DaoUtil { +public final class DaoUtil { - private DaoUtil() { - } + private DaoUtil() {} public static PageData toPageData(Page> page) { List data = convertDataList(page.getContent()); @@ -98,17 +97,17 @@ public abstract class DaoUtil { return PageRequest.of(pageLink.getPage(), pageLink.getPageSize(), pageLink.toSort(sortOrders, columnMap, addDefaultSorting)); } - public static List convertDataList(Collection> toDataList) { - List list = Collections.emptyList(); - if (toDataList != null && !toDataList.isEmpty()) { - list = new ArrayList<>(); - for (ToData object : toDataList) { - if (object != null) { - list.add(object.toData()); - } + public static List convertDataList(Collection> toConvert) { + if (CollectionUtils.isEmpty(toConvert)) { + return Collections.emptyList(); + } + List converted = new ArrayList<>(toConvert.size()); + for (ToData object : toConvert) { + if (object != null) { + converted.add(object.toData()); } } - return list; + return converted; } public static T getData(ToData data) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java index 3875169305..411a67ca98 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.CachedVersionedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; +import org.thingsboard.server.dao.model.sql.AiModelSettingsEntity; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.sql.JpaExecutorService; @@ -92,7 +93,7 @@ class AiModelSettingsServiceImpl extends CachedVersionedEntityService findAiModelSettingsByTenantId(TenantId tenantId, PageLink pageLink) { - validatePageLink(pageLink); + validatePageLink(pageLink, AiModelSettingsEntity.ALLOWED_SORT_PROPERTIES); return aiModelSettingsDao.findAllByTenantId(tenantId, pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java index 7d27a44cd1..2a866c8620 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java @@ -31,8 +31,12 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseVersionedEntity; import org.thingsboard.server.dao.model.ModelConstants; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.UUID; @Getter @@ -43,7 +47,13 @@ import java.util.UUID; public class AiModelSettingsEntity extends BaseVersionedEntity { public static final Map COLUMN_MAP = Map.of( - "createdTime", "created_time" + "createdTime", "created_time", + "provider", "(configuration -> 'providerConfig' ->> 'provider')", + "modelId", "(configuration -> 'modelConfig' ->> 'modelId')" + ); + + public static final Set 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") diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java index e051e99dc5..f94026ce89 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java @@ -26,11 +26,14 @@ import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.dao.exception.IncorrectParameterException; import java.util.List; +import java.util.Set; import java.util.UUID; import java.util.function.Function; import java.util.regex.Pattern; -public class Validator { +public final class Validator { + + private Validator() {} public static final Pattern PROPERTY_PATTERN = Pattern.compile("^[\\p{L}0-9_-]+$"); // Unicode letters, numbers, '_' and '-' allowed @@ -204,22 +207,61 @@ public class Validator { } /** - * This method validate PageLink page link. If pageLink is invalid than throw - * IncorrectParameterException exception + * Validates the specified PageLink object delegating to {@link #validatePageLink(PageLink, Set)} + * with no restrictions on allowed sort properties. * - * @param pageLink the page link + * @param pageLink the PageLink object to validate + * @throws IncorrectParameterException if the pageLink is null, has invalid page size, + * invalid page number, or invalid sort property + * @see #validatePageLink(PageLink, Set) */ public static void validatePageLink(PageLink pageLink) { + validatePageLink(pageLink, null); + } + + /** + * Validates the specified PageLink object ensuring that: + *
    + *
  • The PageLink object is not null
  • + *
  • The page size is greater than zero
  • + *
  • The page number is non-negative
  • + *
  • If sorting is specified, the sort property is valid and allowed
  • + *
+ * + *

When {@code allowedSortProperties} is provided, the sort property + * must be contained within this set. If {@code allowedSortProperties} is null, + * only basic sort property validation is performed. + * + * @param pageLink the PageLink object to validate. + * @param allowedSortProperties a Set of allowed sort property names, or null to skip + * this validation. If provided and the PageLink contains + * a sort order, the sort property must be in this set. + * @throws IncorrectParameterException if any of the following conditions are met: + *

    + *
  • {@code pageLink} is null
  • + *
  • page size is less than 1
  • + *
  • page number is negative
  • + *
  • sort property is malformed
  • + *
  • sort property is not in the {@code allowedSortProperties} set (when the set is provided and not null)
  • + *
+ */ + public static void validatePageLink(PageLink pageLink, Set allowedSortProperties) { if (pageLink == null) { throw new IncorrectParameterException("Page link must be specified."); } else if (pageLink.getPageSize() < 1) { - throw new IncorrectParameterException("Incorrect page link page size '"+pageLink.getPageSize()+"'. Page size must be greater than zero."); + throw new IncorrectParameterException("Incorrect page link page size '" + pageLink.getPageSize() + "'. Page size must be greater than zero."); } else if (pageLink.getPage() < 0) { - throw new IncorrectParameterException("Incorrect page link page '"+pageLink.getPage()+"'. Page must be positive integer."); + throw new IncorrectParameterException("Incorrect page link page '" + pageLink.getPage() + "'. Page must be positive integer."); } else if (pageLink.getSortOrder() != null) { - if (!isValidProperty(pageLink.getSortOrder().getProperty())) { + String sortProperty = pageLink.getSortOrder().getProperty(); + if (!isValidProperty(sortProperty)) { throw new IncorrectParameterException("Invalid page link sort property"); } + if (allowedSortProperties != null && !allowedSortProperties.contains(sortProperty)) { + throw new IncorrectParameterException( + "Unsupported sort property '" + sortProperty + "'. Only '" + String.join("', '", allowedSortProperties) + "' are allowed." + ); + } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java index e7b87b0a1a..a09179d31e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java @@ -17,6 +17,9 @@ package org.thingsboard.server.dao.sql.ai; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +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; @@ -25,6 +28,7 @@ import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.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; @@ -65,15 +69,27 @@ class JpaAiModelSettingsDao extends JpaAbstractDao findByTenantId(UUID tenantId, PageLink pageLink) { return DaoUtil.toPageData(aiModelSettingsRepository.findByTenantId( - tenantId, StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), DaoUtil.toPageable(pageLink, AiModelSettingsEntity.COLUMN_MAP)) + tenantId, StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), toPageRequest(pageLink)) ); } @Override public PageData findIdsByTenantId(UUID tenantId, PageLink pageLink) { - return DaoUtil.pageToPageData( - aiModelSettingsRepository.findIdsByTenantId(tenantId, DaoUtil.toPageable(pageLink, AiModelSettingsEntity.COLUMN_MAP)).map(AiModelSettingsId::new) - ); + return DaoUtil.pageToPageData(aiModelSettingsRepository.findIdsByTenantId(tenantId, toPageRequest(pageLink)).map(AiModelSettingsId::new)); + } + + private static PageRequest toPageRequest(PageLink pageLink) { + Sort sort; + SortOrder sortOrder = pageLink.getSortOrder(); + if (sortOrder == null) { + sort = Sort.by(Sort.Direction.ASC, "id"); + } else { + sort = JpaSort.unsafe( + Sort.Direction.fromString(sortOrder.getDirection().name()), + AiModelSettingsEntity.COLUMN_MAP.getOrDefault(sortOrder.getProperty(), sortOrder.getProperty()) + ); + } + return PageRequest.of(pageLink.getPage(), pageLink.getPageSize(), sort); } @Override From 3f2a6440a75b4a0a7852ba162c4eed5d920abd48 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 27 Jun 2025 15:48:24 +0300 Subject: [PATCH 108/435] AI rule node: support unknown models --- .../data/ai/model/AiModelTypeIdResolver.java | 28 +++++++++++++-- .../common/data/ai/provider/AiProvider.java | 35 ++++++++++++++----- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java index ff31515096..96a5a70390 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java @@ -26,6 +26,7 @@ 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 java.util.Map; @@ -89,6 +90,8 @@ public final class AiModelTypeIdResolver extends TypeIdResolverBase { Map.entry("GITHUB_MODELS::gpt-4o", GitHubModelsChatModel.class) ); + private static final String PROVIDER_MODEL_SEPARATOR = "::"; + private JavaType baseType; @Override @@ -109,9 +112,28 @@ public final class AiModelTypeIdResolver extends TypeIdResolverBase { @Override public JavaType typeFromId(DatabindContext context, String id) { Class modelClass = typeIdToModelClass.get(id); - if (modelClass == null) { // TODO: if provider is unknown - throw, if provider is valid but model is unknown - fallback to default model - throw new IllegalArgumentException("Unknown model type ID: " + id); + if (modelClass != null) { // known model + return context.constructSpecializedType(baseType, modelClass); + } + + String[] parts = id.split(PROVIDER_MODEL_SEPARATOR, 2); + if (parts.length != 2) { + throw new IllegalArgumentException("Invalid model type ID format: " + id + ". Expected format: PROVIDER::MODEL_ID"); + } + + String providerName = parts[0]; + + // Check if the provider exists + AiProvider provider; + try { + provider = AiProvider.valueOf(providerName); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Unknown AI provider: " + providerName); } + + // Provider is valid but model is unknown - fallback to default model class + modelClass = provider.getDefaultModelClass(); + return context.constructSpecializedType(baseType, modelClass); } @@ -123,7 +145,7 @@ public final class AiModelTypeIdResolver extends TypeIdResolverBase { private static String generateId(AiModel model) { String provider = model.providerConfig().provider().name(); String modelId = model.modelConfig().modelId(); - return provider + "::" + modelId; + return provider + PROVIDER_MODEL_SEPARATOR + modelId; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java index d0a5bd0510..f4ae843447 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java @@ -15,15 +15,34 @@ */ package org.thingsboard.server.common.data.ai.provider; +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; + public enum AiProvider { - OPENAI, - AZURE_OPENAI, - GOOGLE_AI_GEMINI, - GOOGLE_VERTEX_AI_GEMINI, - MISTRAL_AI, - ANTHROPIC, - AMAZON_BEDROCK, - GITHUB_MODELS + OPENAI(OpenAiChatModel.class), + AZURE_OPENAI(AzureOpenAiChatModel.class), + GOOGLE_AI_GEMINI(GoogleAiGeminiChatModel.class), + GOOGLE_VERTEX_AI_GEMINI(GoogleVertexAiGeminiChatModel.class), + MISTRAL_AI(MistralAiChatModel.class), + ANTHROPIC(AnthropicChatModel.class), + AMAZON_BEDROCK(AmazonBedrockChatModel.class), + GITHUB_MODELS(GitHubModelsChatModel.class); + + private final Class defaultModelClass; + + AiProvider(Class defaultModelClass) { + this.defaultModelClass = defaultModelClass; + } + + public Class getDefaultModelClass() { + return defaultModelClass; + } } From 3f58ff01c3edfdb0f7ad629b270098503e097f9b Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 27 Jun 2025 16:37:10 +0300 Subject: [PATCH 109/435] AI rule node: support top N for all models --- .../Langchain4jChatModelConfigurerImpl.java | 10 +++++++ .../data/ai/model/chat/AiChatModelConfig.java | 4 --- .../ai/model/chat/AmazonBedrockChatModel.java | 29 ++++--------------- .../ai/model/chat/AnthropicChatModel.java | 29 ++++--------------- .../ai/model/chat/AzureOpenAiChatModel.java | 29 ++++--------------- .../ai/model/chat/GitHubModelsChatModel.java | 29 ++++--------------- .../model/chat/GoogleAiGeminiChatModel.java | 29 ++++--------------- .../chat/GoogleVertexAiGeminiChatModel.java | 29 ++++--------------- .../ai/model/chat/MistralAiChatModel.java | 29 ++++--------------- .../data/ai/model/chat/OpenAiChatModel.java | 29 ++++--------------- 10 files changed, 50 insertions(+), 196 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index 72e9506fdc..97863a0a4a 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -56,6 +56,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .apiKey(chatModel.providerConfig().apiKey()) .modelName(modelConfig.modelId()) .temperature(modelConfig.temperature()) + .topP(modelConfig.topP()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -68,6 +69,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .apiKey(chatModel.providerConfig().apiKey()) .deploymentName(modelConfig.modelId()) .temperature(modelConfig.temperature()) + .topP(modelConfig.topP()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -80,6 +82,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .apiKey(chatModel.providerConfig().apiKey()) .modelName(modelConfig.modelId()) .temperature(modelConfig.temperature()) + .topP(modelConfig.topP()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -112,6 +115,9 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur if (modelConfig.temperature() != null) { generationConfigBuilder.setTemperature(modelConfig.temperature().floatValue()); } + if (modelConfig.topP() != null) { + generationConfigBuilder.setTopP(modelConfig.topP().floatValue()); + } var generationConfig = generationConfigBuilder.build(); // construct generative model instance @@ -128,6 +134,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .apiKey(chatModel.providerConfig().apiKey()) .modelName(modelConfig.modelId()) .temperature(modelConfig.temperature()) + .topP(modelConfig.topP()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -140,6 +147,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .apiKey(chatModel.providerConfig().apiKey()) .modelName(modelConfig.modelId()) .temperature(modelConfig.temperature()) + .topP(modelConfig.topP()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -161,6 +169,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur var defaultChatRequestParams = ChatRequestParameters.builder() .temperature(modelConfig.temperature()) + .topP(modelConfig.topP()) .build(); return BedrockChatModel.builder() @@ -179,6 +188,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .gitHubToken(chatModel.providerConfig().personalAccessToken()) .modelName(modelConfig.modelId()) .temperature(modelConfig.temperature()) + .topP(modelConfig.topP()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java index d5e794cec0..5562c12740 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java @@ -23,14 +23,10 @@ public sealed interface AiChatModelConfig> extend GoogleVertexAiGeminiChatModel.Config, MistralAiChatModel.Config, AnthropicChatModel.Config, AmazonBedrockChatModel.Config, GitHubModelsChatModel.Config { - Double temperature(); - Integer timeoutSeconds(); Integer maxRetries(); - C withTemperature(Double temperature); - C withTimeoutSeconds(Integer timeoutSeconds); C withMaxRetries(Integer maxRetries); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java index 26eb885078..7d38d6e721 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java @@ -16,47 +16,28 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import lombok.With; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; public record AmazonBedrockChatModel( AiModelType modelType, AmazonBedrockProviderConfig providerConfig, - Config modelConfig + @With Config modelConfig ) implements AiChatModel { + @With public record Config( String modelId, Double temperature, + Double topP, Integer timeoutSeconds, Integer maxRetries - ) implements AiChatModelConfig { - - @Override - public AmazonBedrockChatModel.Config withTemperature(Double temperature) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - @Override - public AmazonBedrockChatModel.Config withTimeoutSeconds(Integer timeoutSeconds) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - @Override - public AmazonBedrockChatModel.Config withMaxRetries(Integer maxRetries) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - } + ) implements AiChatModelConfig {} @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); } - @Override - public AmazonBedrockChatModel withModelConfig(AmazonBedrockChatModel.Config config) { - return new AmazonBedrockChatModel(modelType, providerConfig, config); - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java index 93c195ecab..a15a3f9078 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java @@ -16,47 +16,28 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import lombok.With; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig; public record AnthropicChatModel( AiModelType modelType, AnthropicProviderConfig providerConfig, - Config modelConfig + @With Config modelConfig ) implements AiChatModel { + @With public record Config( String modelId, Double temperature, + Double topP, Integer timeoutSeconds, Integer maxRetries - ) implements AiChatModelConfig { - - @Override - public AnthropicChatModel.Config withTemperature(Double temperature) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - @Override - public AnthropicChatModel.Config withTimeoutSeconds(Integer timeoutSeconds) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - @Override - public AnthropicChatModel.Config withMaxRetries(Integer maxRetries) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - } + ) implements AiChatModelConfig {} @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); } - @Override - public AnthropicChatModel withModelConfig(AnthropicChatModel.Config config) { - return new AnthropicChatModel(modelType, providerConfig, config); - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java index 70e8b26711..6d54975748 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java @@ -16,47 +16,28 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import lombok.With; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; public record AzureOpenAiChatModel( AiModelType modelType, AzureOpenAiProviderConfig providerConfig, - Config modelConfig + @With Config modelConfig ) implements AiChatModel { + @With public record Config( String modelId, Double temperature, + Double topP, Integer timeoutSeconds, Integer maxRetries - ) implements AiChatModelConfig { - - @Override - public AzureOpenAiChatModel.Config withTemperature(Double temperature) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - @Override - public AzureOpenAiChatModel.Config withTimeoutSeconds(Integer timeoutSeconds) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - @Override - public AzureOpenAiChatModel.Config withMaxRetries(Integer maxRetries) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - } + ) implements AiChatModelConfig {} @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); } - @Override - public AzureOpenAiChatModel withModelConfig(AzureOpenAiChatModel.Config config) { - return new AzureOpenAiChatModel(modelType, providerConfig, config); - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java index 828169ec37..075473f111 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java @@ -16,47 +16,28 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import lombok.With; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.GithubModelsProviderConfig; public record GitHubModelsChatModel( AiModelType modelType, GithubModelsProviderConfig providerConfig, - Config modelConfig + @With Config modelConfig ) implements AiChatModel { + @With public record Config( String modelId, Double temperature, + Double topP, Integer timeoutSeconds, Integer maxRetries - ) implements AiChatModelConfig { - - @Override - public GitHubModelsChatModel.Config withTemperature(Double temperature) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - @Override - public GitHubModelsChatModel.Config withTimeoutSeconds(Integer timeoutSeconds) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - @Override - public GitHubModelsChatModel.Config withMaxRetries(Integer maxRetries) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - } + ) implements AiChatModelConfig {} @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); } - @Override - public GitHubModelsChatModel withModelConfig(GitHubModelsChatModel.Config config) { - return new GitHubModelsChatModel(modelType, providerConfig, config); - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java index 80b7e46858..9b1d6b9e91 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java @@ -16,47 +16,28 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import lombok.With; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; public record GoogleAiGeminiChatModel( AiModelType modelType, GoogleAiGeminiProviderConfig providerConfig, - Config modelConfig + @With Config modelConfig ) implements AiChatModel { + @With public record Config( String modelId, Double temperature, + Double topP, Integer timeoutSeconds, Integer maxRetries - ) implements AiChatModelConfig { - - @Override - public Config withTemperature(Double temperature) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - @Override - public Config withTimeoutSeconds(Integer timeoutSeconds) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - @Override - public Config withMaxRetries(Integer maxRetries) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - } + ) implements AiChatModelConfig {} @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); } - @Override - public GoogleAiGeminiChatModel withModelConfig(GoogleAiGeminiChatModel.Config config) { - return new GoogleAiGeminiChatModel(modelType, providerConfig, config); - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java index 201311de23..5659aed918 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java @@ -16,47 +16,28 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import lombok.With; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; public record GoogleVertexAiGeminiChatModel( AiModelType modelType, GoogleVertexAiGeminiProviderConfig providerConfig, - Config modelConfig + @With Config modelConfig ) implements AiChatModel { + @With public record Config( String modelId, Double temperature, + Double topP, Integer timeoutSeconds, // TODO: not supported by Vertex AI Integer maxRetries - ) implements AiChatModelConfig { - - @Override - public Config withTemperature(Double temperature) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - @Override - public Config withTimeoutSeconds(Integer timeoutSeconds) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - @Override - public Config withMaxRetries(Integer maxRetries) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - } + ) implements AiChatModelConfig {} @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); } - @Override - public GoogleVertexAiGeminiChatModel withModelConfig(GoogleVertexAiGeminiChatModel.Config config) { - return new GoogleVertexAiGeminiChatModel(modelType, providerConfig, config); - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java index 493c0582c6..b76ce500f4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java @@ -16,47 +16,28 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import lombok.With; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; public record MistralAiChatModel( AiModelType modelType, MistralAiProviderConfig providerConfig, - Config modelConfig + @With Config modelConfig ) implements AiChatModel { + @With public record Config( String modelId, Double temperature, + Double topP, Integer timeoutSeconds, Integer maxRetries - ) implements AiChatModelConfig { - - @Override - public Config withTemperature(Double temperature) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - @Override - public Config withTimeoutSeconds(Integer timeoutSeconds) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - @Override - public Config withMaxRetries(Integer maxRetries) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - } + ) implements AiChatModelConfig {} @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); } - @Override - public MistralAiChatModel withModelConfig(Config config) { - return new MistralAiChatModel(modelType, providerConfig, config); - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java index 3c138d97b7..d066ee4d50 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java @@ -16,47 +16,28 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import lombok.With; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; public record OpenAiChatModel( AiModelType modelType, OpenAiProviderConfig providerConfig, - Config modelConfig + @With Config modelConfig ) implements AiChatModel { + @With public record Config( String modelId, Double temperature, + Double topP, Integer timeoutSeconds, Integer maxRetries - ) implements AiChatModelConfig { - - @Override - public OpenAiChatModel.Config withTemperature(Double temperature) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - @Override - public OpenAiChatModel.Config withTimeoutSeconds(Integer timeoutSeconds) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - @Override - public OpenAiChatModel.Config withMaxRetries(Integer maxRetries) { - return new Config(modelId, temperature, timeoutSeconds, maxRetries); - } - - } + ) implements AiChatModelConfig {} @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); } - @Override - public OpenAiChatModel withModelConfig(OpenAiChatModel.Config config) { - return new OpenAiChatModel(modelType, providerConfig, config); - } - } From afb0259010529e8cebf9441c851cf826062831cb Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 27 Jun 2025 18:41:21 +0300 Subject: [PATCH 110/435] AI rule node: support timeout for Vertex AI --- .../Langchain4jChatModelConfigurerImpl.java | 48 +++++++++++++++++-- .../chat/GoogleVertexAiGeminiChatModel.java | 2 +- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index 97863a0a4a..156949fdc5 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -15,10 +15,14 @@ */ package org.thingsboard.server.service.ai; +import com.google.api.gax.core.FixedCredentialsProvider; +import com.google.api.gax.retrying.RetrySettings; import com.google.auth.oauth2.ServiceAccountCredentials; import com.google.cloud.vertexai.Transport; import com.google.cloud.vertexai.VertexAI; 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.bedrock.BedrockChatModel; import dev.langchain4j.model.chat.ChatModel; @@ -96,17 +100,44 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur // construct service account credentials using service account key JSON ServiceAccountCredentials serviceAccountCredentials; try { - serviceAccountCredentials = ServiceAccountCredentials - .fromStream(new ByteArrayInputStream(JacksonUtil.writeValueAsBytes(providerConfig.serviceAccountKey()))); + serviceAccountCredentials = ServiceAccountCredentials.fromStream( + new ByteArrayInputStream(JacksonUtil.writeValueAsBytes(providerConfig.serviceAccountKey())) + ); } catch (IOException e) { throw new RuntimeException("Failed to parse service account key JSON", e); } + PredictionServiceSettings predictionServiceClientSettings; + try { + // create prediction service settings for REST transport with service account key credentials + PredictionServiceSettings.Builder settingsBuilder = PredictionServiceSettings.newHttpJsonBuilder() + .setCredentialsProvider(FixedCredentialsProvider.create(serviceAccountCredentials)); + + // get the retry settings that control request timeout for generateContent RPC + RetrySettings.Builder retrySettings = settingsBuilder + .generateContentSettings() + .getRetrySettings() + .toBuilder(); + + // set request timeout from model config + if (modelConfig.timeoutSeconds() != null) { + retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(modelConfig.timeoutSeconds())); + } + + // set updated retry settings + settingsBuilder.generateContentSettings().setRetrySettings(retrySettings.build()); + + // build the client settings + predictionServiceClientSettings = settingsBuilder.build(); + } catch (IOException e) { + throw new RuntimeException("Failed to create prediction service client settings", e); + } + // construct Vertex AI instance var vertexAI = new VertexAI.Builder() .setProjectId(providerConfig.projectId()) .setLocation(providerConfig.location()) - .setCredentials(serviceAccountCredentials) + .setPredictionClientSupplier(() -> createPredictionServiceClient(predictionServiceClientSettings)) .setTransport(Transport.REST) // GRPC also possible, but likely does not work with service account keys .build(); @@ -121,12 +152,19 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur var generationConfig = generationConfigBuilder.build(); // construct generative model instance - var generativeModel = new GenerativeModel(modelConfig.modelId(), vertexAI) - .withGenerationConfig(generationConfig); + var generativeModel = new GenerativeModel(modelConfig.modelId(), vertexAI).withGenerationConfig(generationConfig); return new VertexAiGeminiChatModel(generativeModel, generationConfig, modelConfig.maxRetries()); } + private static PredictionServiceClient createPredictionServiceClient(PredictionServiceSettings settings) { + try { + return PredictionServiceClient.create(settings); + } catch (IOException e) { + throw new RuntimeException("Failed to create prediction service client", e); + } + } + @Override public ChatModel configureChatModel(MistralAiChatModel chatModel) { MistralAiChatModel.Config modelConfig = chatModel.modelConfig(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java index 5659aed918..67a852e33c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java @@ -31,7 +31,7 @@ public record GoogleVertexAiGeminiChatModel( String modelId, Double temperature, Double topP, - Integer timeoutSeconds, // TODO: not supported by Vertex AI + Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} From 72db8f9823e86899e35dd900ec68fa9d34cf9572 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 27 Jun 2025 19:02:44 +0300 Subject: [PATCH 111/435] AI rule node: support top K for Google AI Gemini, Google Vertex AI Gemini and Anthropic --- .../service/ai/Langchain4jChatModelConfigurerImpl.java | 5 +++++ .../server/common/data/ai/model/chat/AnthropicChatModel.java | 1 + .../common/data/ai/model/chat/GoogleAiGeminiChatModel.java | 1 + .../data/ai/model/chat/GoogleVertexAiGeminiChatModel.java | 1 + 4 files changed, 8 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index 156949fdc5..2a9c792b32 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -87,6 +87,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .modelName(modelConfig.modelId()) .temperature(modelConfig.temperature()) .topP(modelConfig.topP()) + .topK(modelConfig.topK()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -149,6 +150,9 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur if (modelConfig.topP() != null) { generationConfigBuilder.setTopP(modelConfig.topP().floatValue()); } + if (modelConfig.topK() != null) { + generationConfigBuilder.setTopK(modelConfig.topK()); + } var generationConfig = generationConfigBuilder.build(); // construct generative model instance @@ -186,6 +190,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .modelName(modelConfig.modelId()) .temperature(modelConfig.temperature()) .topP(modelConfig.topP()) + .topK(modelConfig.topK()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java index a15a3f9078..ff218d99af 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java @@ -31,6 +31,7 @@ public record AnthropicChatModel( String modelId, Double temperature, Double topP, + Integer topK, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java index 9b1d6b9e91..e5d282ee50 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java @@ -31,6 +31,7 @@ public record GoogleAiGeminiChatModel( String modelId, Double temperature, Double topP, + Integer topK, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java index 67a852e33c..2f5fac23c2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java @@ -31,6 +31,7 @@ public record GoogleVertexAiGeminiChatModel( String modelId, Double temperature, Double topP, + Integer topK, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} From dfe4dea436fde3f5f17876fc167f0d8683476451 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 27 Jun 2025 19:16:13 +0300 Subject: [PATCH 112/435] AI rule node: add frequency penalty for the models that support it --- .../service/ai/Langchain4jChatModelConfigurerImpl.java | 8 ++++++++ .../common/data/ai/model/chat/AzureOpenAiChatModel.java | 1 + .../common/data/ai/model/chat/GitHubModelsChatModel.java | 1 + .../data/ai/model/chat/GoogleAiGeminiChatModel.java | 1 + .../data/ai/model/chat/GoogleVertexAiGeminiChatModel.java | 1 + .../common/data/ai/model/chat/MistralAiChatModel.java | 1 + .../server/common/data/ai/model/chat/OpenAiChatModel.java | 1 + 7 files changed, 14 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index 2a9c792b32..f7e5203e1b 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -61,6 +61,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .modelName(modelConfig.modelId()) .temperature(modelConfig.temperature()) .topP(modelConfig.topP()) + .frequencyPenalty(modelConfig.frequencyPenalty()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -74,6 +75,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .deploymentName(modelConfig.modelId()) .temperature(modelConfig.temperature()) .topP(modelConfig.topP()) + .frequencyPenalty(modelConfig.frequencyPenalty()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -88,6 +90,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .temperature(modelConfig.temperature()) .topP(modelConfig.topP()) .topK(modelConfig.topK()) + .frequencyPenalty(modelConfig.frequencyPenalty()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -153,6 +156,9 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur if (modelConfig.topK() != null) { generationConfigBuilder.setTopK(modelConfig.topK()); } + if (modelConfig.frequencyPenalty() != null) { + generationConfigBuilder.setFrequencyPenalty(modelConfig.frequencyPenalty().floatValue()); + } var generationConfig = generationConfigBuilder.build(); // construct generative model instance @@ -177,6 +183,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .modelName(modelConfig.modelId()) .temperature(modelConfig.temperature()) .topP(modelConfig.topP()) + .frequencyPenalty(modelConfig.frequencyPenalty()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -232,6 +239,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .modelName(modelConfig.modelId()) .temperature(modelConfig.temperature()) .topP(modelConfig.topP()) + .frequencyPenalty(modelConfig.frequencyPenalty()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java index 6d54975748..97f2d0c3ac 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java @@ -31,6 +31,7 @@ public record AzureOpenAiChatModel( String modelId, Double temperature, Double topP, + Double frequencyPenalty, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java index 075473f111..3730c9aab9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java @@ -31,6 +31,7 @@ public record GitHubModelsChatModel( String modelId, Double temperature, Double topP, + Double frequencyPenalty, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java index e5d282ee50..a0e8ef04ee 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java @@ -32,6 +32,7 @@ public record GoogleAiGeminiChatModel( Double temperature, Double topP, Integer topK, + Double frequencyPenalty, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java index 2f5fac23c2..7acf566b46 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java @@ -32,6 +32,7 @@ public record GoogleVertexAiGeminiChatModel( Double temperature, Double topP, Integer topK, + Double frequencyPenalty, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java index b76ce500f4..04c5e96943 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java @@ -31,6 +31,7 @@ public record MistralAiChatModel( String modelId, Double temperature, Double topP, + Double frequencyPenalty, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java index d066ee4d50..497650fe3c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java @@ -31,6 +31,7 @@ public record OpenAiChatModel( String modelId, Double temperature, Double topP, + Double frequencyPenalty, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} From bc96c63fcfff6d3d8e4bda65e986f68015c46f1c Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 27 Jun 2025 19:27:52 +0300 Subject: [PATCH 113/435] AI rule node: add presence penalty for the models that support it --- .../service/ai/Langchain4jChatModelConfigurerImpl.java | 8 ++++++++ .../common/data/ai/model/chat/AzureOpenAiChatModel.java | 1 + .../common/data/ai/model/chat/GitHubModelsChatModel.java | 1 + .../data/ai/model/chat/GoogleAiGeminiChatModel.java | 1 + .../data/ai/model/chat/GoogleVertexAiGeminiChatModel.java | 1 + .../common/data/ai/model/chat/MistralAiChatModel.java | 1 + .../server/common/data/ai/model/chat/OpenAiChatModel.java | 1 + 7 files changed, 14 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index f7e5203e1b..c8022e518c 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -62,6 +62,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .temperature(modelConfig.temperature()) .topP(modelConfig.topP()) .frequencyPenalty(modelConfig.frequencyPenalty()) + .presencePenalty(modelConfig.presencePenalty()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -76,6 +77,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .temperature(modelConfig.temperature()) .topP(modelConfig.topP()) .frequencyPenalty(modelConfig.frequencyPenalty()) + .presencePenalty(modelConfig.presencePenalty()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -91,6 +93,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .topP(modelConfig.topP()) .topK(modelConfig.topK()) .frequencyPenalty(modelConfig.frequencyPenalty()) + .presencePenalty(modelConfig.presencePenalty()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -159,6 +162,9 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur if (modelConfig.frequencyPenalty() != null) { generationConfigBuilder.setFrequencyPenalty(modelConfig.frequencyPenalty().floatValue()); } + if (modelConfig.frequencyPenalty() != null) { + generationConfigBuilder.setPresencePenalty(modelConfig.frequencyPenalty().floatValue()); + } var generationConfig = generationConfigBuilder.build(); // construct generative model instance @@ -184,6 +190,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .temperature(modelConfig.temperature()) .topP(modelConfig.topP()) .frequencyPenalty(modelConfig.frequencyPenalty()) + .presencePenalty(modelConfig.presencePenalty()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -240,6 +247,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .temperature(modelConfig.temperature()) .topP(modelConfig.topP()) .frequencyPenalty(modelConfig.frequencyPenalty()) + .presencePenalty(modelConfig.presencePenalty()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java index 97f2d0c3ac..c6ba4c9b80 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java @@ -32,6 +32,7 @@ public record AzureOpenAiChatModel( Double temperature, Double topP, Double frequencyPenalty, + Double presencePenalty, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java index 3730c9aab9..24ece4ec16 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java @@ -32,6 +32,7 @@ public record GitHubModelsChatModel( Double temperature, Double topP, Double frequencyPenalty, + Double presencePenalty, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java index a0e8ef04ee..ffed1c7de0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java @@ -33,6 +33,7 @@ public record GoogleAiGeminiChatModel( Double topP, Integer topK, Double frequencyPenalty, + Double presencePenalty, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java index 7acf566b46..16744d3b90 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java @@ -33,6 +33,7 @@ public record GoogleVertexAiGeminiChatModel( Double topP, Integer topK, Double frequencyPenalty, + Double presencePenalty, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java index 04c5e96943..c44e7b4ec8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java @@ -32,6 +32,7 @@ public record MistralAiChatModel( Double temperature, Double topP, Double frequencyPenalty, + Double presencePenalty, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java index 497650fe3c..439786abda 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java @@ -32,6 +32,7 @@ public record OpenAiChatModel( Double temperature, Double topP, Double frequencyPenalty, + Double presencePenalty, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} From d81d41fd7bfe5d2f5e6be6a5aa9439c96fdaaea0 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 27 Jun 2025 19:37:12 +0300 Subject: [PATCH 114/435] AI rule node: add max output tokens for all providers --- .../service/ai/Langchain4jChatModelConfigurerImpl.java | 10 ++++++++++ .../data/ai/model/chat/AmazonBedrockChatModel.java | 1 + .../common/data/ai/model/chat/AnthropicChatModel.java | 1 + .../data/ai/model/chat/AzureOpenAiChatModel.java | 1 + .../data/ai/model/chat/GitHubModelsChatModel.java | 1 + .../data/ai/model/chat/GoogleAiGeminiChatModel.java | 1 + .../ai/model/chat/GoogleVertexAiGeminiChatModel.java | 1 + .../common/data/ai/model/chat/MistralAiChatModel.java | 1 + .../common/data/ai/model/chat/OpenAiChatModel.java | 1 + 9 files changed, 18 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index c8022e518c..fdcea9df18 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -63,6 +63,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .topP(modelConfig.topP()) .frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(modelConfig.presencePenalty()) + .maxTokens(modelConfig.maxOutputTokens()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -78,6 +79,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .topP(modelConfig.topP()) .frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(modelConfig.presencePenalty()) + .maxTokens(modelConfig.maxOutputTokens()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -94,6 +96,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .topK(modelConfig.topK()) .frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(modelConfig.presencePenalty()) + .maxOutputTokens(modelConfig.maxOutputTokens()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -165,6 +168,9 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur if (modelConfig.frequencyPenalty() != null) { generationConfigBuilder.setPresencePenalty(modelConfig.frequencyPenalty().floatValue()); } + if (modelConfig.maxOutputTokens() != null) { + generationConfigBuilder.setMaxOutputTokens(modelConfig.maxOutputTokens()); + } var generationConfig = generationConfigBuilder.build(); // construct generative model instance @@ -191,6 +197,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .topP(modelConfig.topP()) .frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(modelConfig.presencePenalty()) + .maxTokens(modelConfig.maxOutputTokens()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -205,6 +212,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .temperature(modelConfig.temperature()) .topP(modelConfig.topP()) .topK(modelConfig.topK()) + .maxTokens(modelConfig.maxOutputTokens()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -227,6 +235,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur var defaultChatRequestParams = ChatRequestParameters.builder() .temperature(modelConfig.temperature()) .topP(modelConfig.topP()) + .maxOutputTokens(modelConfig.maxOutputTokens()) .build(); return BedrockChatModel.builder() @@ -248,6 +257,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .topP(modelConfig.topP()) .frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(modelConfig.presencePenalty()) + .maxTokens(modelConfig.maxOutputTokens()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java index 7d38d6e721..cd4a27326b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java @@ -31,6 +31,7 @@ public record AmazonBedrockChatModel( String modelId, Double temperature, Double topP, + Integer maxOutputTokens, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java index ff218d99af..eff282645a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java @@ -32,6 +32,7 @@ public record AnthropicChatModel( Double temperature, Double topP, Integer topK, + Integer maxOutputTokens, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java index c6ba4c9b80..ff59f042c6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java @@ -33,6 +33,7 @@ public record AzureOpenAiChatModel( Double topP, Double frequencyPenalty, Double presencePenalty, + Integer maxOutputTokens, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java index 24ece4ec16..684c8133b7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java @@ -33,6 +33,7 @@ public record GitHubModelsChatModel( Double topP, Double frequencyPenalty, Double presencePenalty, + Integer maxOutputTokens, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java index ffed1c7de0..7618032837 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java @@ -34,6 +34,7 @@ public record GoogleAiGeminiChatModel( Integer topK, Double frequencyPenalty, Double presencePenalty, + Integer maxOutputTokens, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java index 16744d3b90..8755760fe6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java @@ -34,6 +34,7 @@ public record GoogleVertexAiGeminiChatModel( Integer topK, Double frequencyPenalty, Double presencePenalty, + Integer maxOutputTokens, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java index c44e7b4ec8..c896b2c158 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java @@ -33,6 +33,7 @@ public record MistralAiChatModel( Double topP, Double frequencyPenalty, Double presencePenalty, + Integer maxOutputTokens, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java index 439786abda..435e543595 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java @@ -33,6 +33,7 @@ public record OpenAiChatModel( Double topP, Double frequencyPenalty, Double presencePenalty, + Integer maxOutputTokens, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} From 0960b4b1799fef7bbff2933bed4438ad7b20d5ff Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 27 Jun 2025 19:46:37 +0300 Subject: [PATCH 115/435] AI rule node: add stop sequences for all providers --- .../service/ai/Langchain4jChatModelConfigurerImpl.java | 10 ++++++++++ .../data/ai/model/chat/AmazonBedrockChatModel.java | 3 +++ .../common/data/ai/model/chat/AnthropicChatModel.java | 3 +++ .../data/ai/model/chat/AzureOpenAiChatModel.java | 3 +++ .../data/ai/model/chat/GitHubModelsChatModel.java | 3 +++ .../data/ai/model/chat/GoogleAiGeminiChatModel.java | 3 +++ .../ai/model/chat/GoogleVertexAiGeminiChatModel.java | 3 +++ .../common/data/ai/model/chat/MistralAiChatModel.java | 3 +++ .../common/data/ai/model/chat/OpenAiChatModel.java | 3 +++ 9 files changed, 34 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index fdcea9df18..30942beda7 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -64,6 +64,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(modelConfig.presencePenalty()) .maxTokens(modelConfig.maxOutputTokens()) + .stop(modelConfig.stopSequences()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -80,6 +81,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(modelConfig.presencePenalty()) .maxTokens(modelConfig.maxOutputTokens()) + .stop(modelConfig.stopSequences()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -97,6 +99,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(modelConfig.presencePenalty()) .maxOutputTokens(modelConfig.maxOutputTokens()) + .stopSequences(modelConfig.stopSequences()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -171,6 +174,9 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur if (modelConfig.maxOutputTokens() != null) { generationConfigBuilder.setMaxOutputTokens(modelConfig.maxOutputTokens()); } + if (modelConfig.stopSequences() != null) { + generationConfigBuilder.addAllStopSequences(modelConfig.stopSequences()); + } var generationConfig = generationConfigBuilder.build(); // construct generative model instance @@ -198,6 +204,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(modelConfig.presencePenalty()) .maxTokens(modelConfig.maxOutputTokens()) + .stopSequences(modelConfig.stopSequences()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -213,6 +220,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .topP(modelConfig.topP()) .topK(modelConfig.topK()) .maxTokens(modelConfig.maxOutputTokens()) + .stopSequences(modelConfig.stopSequences()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); @@ -236,6 +244,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .temperature(modelConfig.temperature()) .topP(modelConfig.topP()) .maxOutputTokens(modelConfig.maxOutputTokens()) + .stopSequences(modelConfig.stopSequences()) .build(); return BedrockChatModel.builder() @@ -258,6 +267,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(modelConfig.presencePenalty()) .maxTokens(modelConfig.maxOutputTokens()) + .stop(modelConfig.stopSequences()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java index cd4a27326b..422f646f8f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java @@ -20,6 +20,8 @@ import lombok.With; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; +import java.util.List; + public record AmazonBedrockChatModel( AiModelType modelType, AmazonBedrockProviderConfig providerConfig, @@ -32,6 +34,7 @@ public record AmazonBedrockChatModel( Double temperature, Double topP, Integer maxOutputTokens, + List stopSequences, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java index eff282645a..3258229372 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java @@ -20,6 +20,8 @@ import lombok.With; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig; +import java.util.List; + public record AnthropicChatModel( AiModelType modelType, AnthropicProviderConfig providerConfig, @@ -33,6 +35,7 @@ public record AnthropicChatModel( Double topP, Integer topK, Integer maxOutputTokens, + List stopSequences, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java index ff59f042c6..2e783d26d1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java @@ -20,6 +20,8 @@ import lombok.With; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; +import java.util.List; + public record AzureOpenAiChatModel( AiModelType modelType, AzureOpenAiProviderConfig providerConfig, @@ -34,6 +36,7 @@ public record AzureOpenAiChatModel( Double frequencyPenalty, Double presencePenalty, Integer maxOutputTokens, + List stopSequences, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java index 684c8133b7..cc981cc5c5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java @@ -20,6 +20,8 @@ import lombok.With; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.GithubModelsProviderConfig; +import java.util.List; + public record GitHubModelsChatModel( AiModelType modelType, GithubModelsProviderConfig providerConfig, @@ -34,6 +36,7 @@ public record GitHubModelsChatModel( Double frequencyPenalty, Double presencePenalty, Integer maxOutputTokens, + List stopSequences, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java index 7618032837..e5fba391b0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java @@ -20,6 +20,8 @@ import lombok.With; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; +import java.util.List; + public record GoogleAiGeminiChatModel( AiModelType modelType, GoogleAiGeminiProviderConfig providerConfig, @@ -35,6 +37,7 @@ public record GoogleAiGeminiChatModel( Double frequencyPenalty, Double presencePenalty, Integer maxOutputTokens, + List stopSequences, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java index 8755760fe6..f0e5834d54 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java @@ -20,6 +20,8 @@ import lombok.With; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; +import java.util.List; + public record GoogleVertexAiGeminiChatModel( AiModelType modelType, GoogleVertexAiGeminiProviderConfig providerConfig, @@ -35,6 +37,7 @@ public record GoogleVertexAiGeminiChatModel( Double frequencyPenalty, Double presencePenalty, Integer maxOutputTokens, + List stopSequences, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java index c896b2c158..ac1d644dcc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java @@ -20,6 +20,8 @@ import lombok.With; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; +import java.util.List; + public record MistralAiChatModel( AiModelType modelType, MistralAiProviderConfig providerConfig, @@ -34,6 +36,7 @@ public record MistralAiChatModel( Double frequencyPenalty, Double presencePenalty, Integer maxOutputTokens, + List stopSequences, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java index 435e543595..8a5556b309 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java @@ -20,6 +20,8 @@ import lombok.With; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; +import java.util.List; + public record OpenAiChatModel( AiModelType modelType, OpenAiProviderConfig providerConfig, @@ -34,6 +36,7 @@ public record OpenAiChatModel( Double frequencyPenalty, Double presencePenalty, Integer maxOutputTokens, + List stopSequences, Integer timeoutSeconds, Integer maxRetries ) implements AiChatModelConfig {} From 4d4d16520dfaa8279aee7139efb3a00968af74a1 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 30 Jun 2025 17:45:04 +0300 Subject: [PATCH 116/435] UI: Fixed detect changes for tables --- .../components/widget/lib/alarm/alarms-table-widget.component.ts | 1 + .../widget/lib/entity/entities-table-widget.component.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts index 45825a0294..b1c0f38ef2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts @@ -340,6 +340,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, public onDataUpdated() { this.alarmsDatasource.updateAlarms(); this.clearCache(); + this.ctx.detectChanges(); } public onEditModeChanged() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts index 82537384a1..f95c8c26d7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts @@ -275,6 +275,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni public onDataUpdated() { this.entityDatasource.dataUpdated(); this.clearCache(); + this.ctx.detectChanges(); } public onEditModeChanged() { From 68be1163b66a2d0d0d24447581f244afacbcf642 Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Tue, 1 Jul 2025 11:09:03 +0300 Subject: [PATCH 117/435] Cleanup upgrade from 4.1 to 4.2 --- .../install/ThingsboardInstallService.java | 1 - .../DefaultDatabaseSchemaSettingsService.java | 2 +- .../update/DefaultDataUpdateService.java | 101 ++---------------- .../common/data/limit/RateLimitUtil.java | 12 --- .../DefaultTenantProfileConfiguration.java | 40 ------- 5 files changed, 7 insertions(+), 149 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 5e5185ac8b..6765e95246 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -116,7 +116,6 @@ public class ThingsboardInstallService { entityDatabaseSchemaService.createDatabaseIndexes(); // TODO: cleanup update code after each release - systemDataLoaderService.updateDefaultNotificationConfigs(false); // Runs upgrade scripts that are not possible in plain SQL. dataUpdateService.updateData(); diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java index 90c5966f3f..e5bd026fb7 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java @@ -32,7 +32,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti // This list should include all versions which are compatible for the upgrade. // The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release. - private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.0.0", "4.0.1"); + private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.1.0"); private final ProjectInfo projectInfo; private final JdbcTemplate jdbcTemplate; diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index c3f1cee046..972d5ff36c 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -20,25 +20,17 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.FilterPredicateValue; -import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.common.data.rule.RuleNode; -import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; -import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.RuleNodeClassInfo; import org.thingsboard.server.service.install.DbUpgradeExecutorService; @@ -46,108 +38,27 @@ import org.thingsboard.server.utils.TbNodeUpgradeUtils; import java.util.ArrayList; import java.util.List; -import java.util.Optional; -import java.util.UUID; import java.util.concurrent.ExecutionException; -import static org.thingsboard.server.dao.rule.BaseRuleChainService.TB_RULE_CHAIN_INPUT_NODE; - @Service @Profile("install") @Slf4j +@RequiredArgsConstructor public class DefaultDataUpdateService implements DataUpdateService { private static final int MAX_PENDING_SAVE_RULE_NODE_FUTURES = 256; private static final int DEFAULT_PAGE_SIZE = 1024; - @Autowired - private RuleChainService ruleChainService; - - @Autowired - private RelationService relationService; - - @Autowired - private ComponentDiscoveryService componentDiscoveryService; - - @Autowired - private DbUpgradeExecutorService executorService; - - @Autowired - private TenantProfileService tenantProfileService; + private final RuleChainService ruleChainService; + private final ComponentDiscoveryService componentDiscoveryService; + private final DbUpgradeExecutorService executorService; @Override public void updateData() throws Exception { log.info("Updating data ..."); //TODO: should be cleaned after each release - updateInputNodes(); - deduplicateRateLimitsPerSecondsConfigurations(); - log.info("Data updated."); - } - - private void deduplicateRateLimitsPerSecondsConfigurations() { - log.info("Starting update of tenant profiles..."); - int totalProfiles = 0; - int updatedTenantProfiles = 0; - int skippedProfiles = 0; - int failedProfiles = 0; - - var tenantProfiles = new PageDataIterable<>( - pageLink -> tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, pageLink), 1024); - - for (TenantProfile tenantProfile : tenantProfiles) { - totalProfiles++; - String profileName = tenantProfile.getName(); - UUID profileId = tenantProfile.getId().getId(); - try { - Optional profileConfiguration = tenantProfile.getProfileConfiguration(); - if (profileConfiguration.isEmpty()) { - log.debug("[{}][{}] Skipping tenant profile with non-default configuration.", profileId, profileName); - skippedProfiles++; - continue; - } - - DefaultTenantProfileConfiguration defaultTenantProfileConfiguration = profileConfiguration.get(); - defaultTenantProfileConfiguration.deduplicateRateLimitsConfigs(); - tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); - updatedTenantProfiles++; - log.debug("[{}][{}] Successfully updated tenant profile.", profileId, profileName); - } catch (Exception e) { - log.error("[{}][{}] Failed to updated tenant profile: ", profileId, profileName, e); - failedProfiles++; - } - } - - log.info("Tenant profiles update completed. Total: {}, Updated: {}, Skipped: {}, Failed: {}", - totalProfiles, updatedTenantProfiles, skippedProfiles, failedProfiles); - } - - - private void updateInputNodes() { - log.info("Creating relations for input nodes..."); - int n = 0; - var inputNodes = new PageDataIterable<>(pageLink -> ruleChainService.findAllRuleNodesByType(TB_RULE_CHAIN_INPUT_NODE, pageLink), 1024); - for (RuleNode inputNode : inputNodes) { - try { - RuleChainId targetRuleChainId = Optional.ofNullable(inputNode.getConfiguration().get("ruleChainId")) - .filter(JsonNode::isTextual).map(JsonNode::asText).map(id -> new RuleChainId(UUID.fromString(id))) - .orElse(null); - if (targetRuleChainId == null) { - continue; - } - - EntityRelation relation = new EntityRelation(); - relation.setFrom(inputNode.getRuleChainId()); - relation.setTo(targetRuleChainId); - relation.setType(EntityRelation.USES_TYPE); - relation.setTypeGroup(RelationTypeGroup.COMMON); - relationService.saveRelation(TenantId.SYS_TENANT_ID, relation); - n++; - } catch (Exception e) { - log.error("Failed to save relation for input node: {}", inputNode, e); - } - } - log.info("Created {} relations for input nodes", n); + log.info("Data updated."); } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitUtil.java index f2ee164dcd..f6e9085489 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitUtil.java @@ -77,16 +77,4 @@ public class RateLimitUtil { return true; } - @Deprecated(forRemoval = true, since = "4.1") - public static String deduplicateByDuration(String configStr) { - if (configStr == null) { - return null; - } - Set distinctDurations = new HashSet<>(); - return parseConfig(configStr).stream() - .filter(entry -> distinctDurations.add(entry.durationSeconds())) - .map(RateLimitEntry::toString) - .collect(Collectors.joining(",")); - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index a4ff47c340..246fe46791 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -24,7 +24,6 @@ import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.TenantProfileType; -import org.thingsboard.server.common.data.limit.RateLimitUtil; import org.thingsboard.server.common.data.validation.RateLimit; import java.io.Serial; @@ -236,43 +235,4 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura return maxRuleNodeExecutionsPerMessage; } - @Deprecated(forRemoval = true, since = "4.1") - public void deduplicateRateLimitsConfigs() { - this.transportTenantMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportTenantMsgRateLimit); - this.transportTenantTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportTenantTelemetryMsgRateLimit); - this.transportTenantTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportTenantTelemetryDataPointsRateLimit); - - this.transportDeviceMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportDeviceMsgRateLimit); - this.transportDeviceTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportDeviceTelemetryMsgRateLimit); - this.transportDeviceTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportDeviceTelemetryDataPointsRateLimit); - - this.transportGatewayMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayMsgRateLimit); - this.transportGatewayTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayTelemetryMsgRateLimit); - this.transportGatewayTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayTelemetryDataPointsRateLimit); - - this.transportGatewayDeviceMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayDeviceMsgRateLimit); - this.transportGatewayDeviceTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayDeviceTelemetryMsgRateLimit); - this.transportGatewayDeviceTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayDeviceTelemetryDataPointsRateLimit); - - this.tenantEntityExportRateLimit = RateLimitUtil.deduplicateByDuration(tenantEntityExportRateLimit); - this.tenantEntityImportRateLimit = RateLimitUtil.deduplicateByDuration(tenantEntityImportRateLimit); - this.tenantNotificationRequestsRateLimit = RateLimitUtil.deduplicateByDuration(tenantNotificationRequestsRateLimit); - this.tenantNotificationRequestsPerRuleRateLimit = RateLimitUtil.deduplicateByDuration(tenantNotificationRequestsPerRuleRateLimit); - - this.cassandraReadQueryTenantCoreRateLimits = RateLimitUtil.deduplicateByDuration(cassandraReadQueryTenantCoreRateLimits); - this.cassandraWriteQueryTenantCoreRateLimits = RateLimitUtil.deduplicateByDuration(cassandraWriteQueryTenantCoreRateLimits); - this.cassandraReadQueryTenantRuleEngineRateLimits = RateLimitUtil.deduplicateByDuration(cassandraReadQueryTenantRuleEngineRateLimits); - this.cassandraWriteQueryTenantRuleEngineRateLimits = RateLimitUtil.deduplicateByDuration(cassandraWriteQueryTenantRuleEngineRateLimits); - - this.edgeEventRateLimits = RateLimitUtil.deduplicateByDuration(edgeEventRateLimits); - this.edgeEventRateLimitsPerEdge = RateLimitUtil.deduplicateByDuration(edgeEventRateLimitsPerEdge); - this.edgeUplinkMessagesRateLimits = RateLimitUtil.deduplicateByDuration(edgeUplinkMessagesRateLimits); - this.edgeUplinkMessagesRateLimitsPerEdge = RateLimitUtil.deduplicateByDuration(edgeUplinkMessagesRateLimitsPerEdge); - - this.wsUpdatesPerSessionRateLimit = RateLimitUtil.deduplicateByDuration(wsUpdatesPerSessionRateLimit); - - this.tenantServerRestLimitsConfiguration = RateLimitUtil.deduplicateByDuration(tenantServerRestLimitsConfiguration); - this.customerServerRestLimitsConfiguration = RateLimitUtil.deduplicateByDuration(customerServerRestLimitsConfiguration); - } - } From 80ebd784d8b11cb2d58d80063354197543d7368e Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 1 Jul 2025 12:18:43 +0300 Subject: [PATCH 118/435] AI rule node: make system prompt optional --- .../java/org/thingsboard/rule/engine/ai/TbAiNode.java | 11 ++++++++--- .../rule/engine/ai/TbAiNodeConfiguration.java | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index edcf2065eb..75faf00c04 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.ai; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.FutureCallback; +import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.ChatModel; @@ -44,6 +45,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.dao.exception.DataValidationException; +import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; @@ -106,11 +108,14 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { public void onMsg(TbContext ctx, TbMsg msg) { var ackedMsg = ackIfNeeded(ctx, msg); - var systemMessage = SystemMessage.from(TbNodeUtils.processPattern(systemPrompt, ackedMsg)); - var userMessage = UserMessage.from(TbNodeUtils.processPattern(userPrompt, ackedMsg)); + List chatMessages = new ArrayList<>(2); + if (systemPrompt != null) { + chatMessages.add(SystemMessage.from(TbNodeUtils.processPattern(systemPrompt, ackedMsg))); + } + chatMessages.add(UserMessage.from(TbNodeUtils.processPattern(userPrompt, ackedMsg))); var chatRequest = ChatRequest.builder() - .messages(List.of(systemMessage, userMessage)) + .messages(chatMessages) .responseFormat(responseFormat) .build(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index 4c99b6f613..1a6bf935a3 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -23,6 +23,7 @@ import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; import lombok.Data; import org.thingsboard.common.util.JsonSchemaUtils; import org.thingsboard.rule.engine.api.NodeConfiguration; @@ -35,7 +36,7 @@ public class TbAiNodeConfiguration implements NodeConfiguration Date: Tue, 1 Jul 2025 12:46:27 +0300 Subject: [PATCH 119/435] UI: Fixed range-chart widget import --- .../components/widget/lib/chart/range-chart-widget.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts index f5ea7487ed..f471a9e84c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts @@ -23,6 +23,7 @@ import { OnDestroy, OnInit, Renderer2, + TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core'; From 7dc8b78202a64c3996165a6d48f7afa3ecaadd94 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 1 Jul 2025 14:18:42 +0300 Subject: [PATCH 120/435] AI rule node: use fixed-size thread pool with unbounded queue --- .../server/service/ai/DefaultAiRequestsExecutor.java | 8 +++----- application/src/main/resources/thingsboard.yml | 2 -- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java b/application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java index ae0663eefc..077efa227d 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java @@ -32,10 +32,11 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; -import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.rule.engine.api.AiRequestsExecutor; import java.time.Duration; +import java.util.concurrent.Executors; @Lazy @Component @@ -56,9 +57,6 @@ class DefaultAiRequestsExecutor implements AiRequestsExecutor { @Min(value = 1, message = "Pool size must be at least 1") private int poolSize = 50; - @Min(value = 1, message = "Max queue size must be at least 1") - private int maxQueueSize = 10000; - @Min(value = 1, message = "Termination timeout must be at least 1 second") private int terminationTimeoutSeconds = 60; @@ -69,7 +67,7 @@ class DefaultAiRequestsExecutor implements AiRequestsExecutor { @PostConstruct private void init() { executorService = MoreExecutors.listeningDecorator( - ThingsBoardExecutors.newLimitedTasksExecutor(properties.getPoolSize(), properties.getMaxQueueSize(), properties.getPoolName()) + Executors.newFixedThreadPool(properties.getPoolSize(), ThingsBoardThreadFactory.forName(properties.getPoolName())) ); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index ee7ff18831..429bbd4a29 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -470,8 +470,6 @@ actors: pool-name: "${ACTORS_RULE_AI_REQUESTS_THREAD_POOL_NAME:ai-requests}" # The maximum number of concurrent HTTP requests pool-size: "${ACTORS_RULE_AI_REQUESTS_THREAD_POOL_SIZE:50}" - # The maximum queue size for pending AI requests - max-queue-size: "${ACTORS_RULE_AI_REQUESTS_THREAD_POOL_QUEUE_SIZE:10000}" # The maximum time in seconds to wait for active tasks to complete during graceful shutdown termination-timeout-seconds: "${ACTORS_RULE_AI_REQUESTS_THREAD_POOL_TERMINATION_TIMEOUT_SECONDS:60}" chain: From 61669dde3f60b7fc08263d81ce7a4c41b2af1f6b Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 1 Jul 2025 14:19:51 +0300 Subject: [PATCH 121/435] AI rule node: increase length limit for prompts --- .../org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index 1a6bf935a3..e5a22219c9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -37,11 +37,11 @@ public class TbAiNodeConfiguration implements NodeConfiguration Date: Tue, 1 Jul 2025 14:23:27 +0300 Subject: [PATCH 122/435] AI rule node: remove redundant SQL from schema_update.sql --- .../main/data/upgrade/basic/schema_update.sql | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 4897a35b41..e4bbe4c69e 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -59,21 +59,3 @@ WHERE trigger_type = 'RATE_LIMITS' AND trigger_config LIKE '%"CASSANDRA_QUERIES"%'; -- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS END - --- CREATE AI MODEL SETTINGS TABLE AND INDEX START - -CREATE TABLE ai_model_settings ( - id UUID NOT NULL PRIMARY KEY, - external_id UUID, - created_time BIGINT NOT NULL, - tenant_id UUID NOT NULL, - 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) -); - -CREATE INDEX idx_ai_model_settings_tenant_id ON ai_model_settings(tenant_id); - --- CREATE AI MODEL SETTINGS TABLE AND INDEX END From 36e16bb877de87a9da918559ea62ceca0879c675 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 1 Jul 2025 15:43:13 +0300 Subject: [PATCH 123/435] AI rule node: improve cache eviction --- .../ai/AiModelSettingsCacheEvictEvent.java | 28 +++++++++++++++---- .../dao/ai/AiModelSettingsServiceImpl.java | 21 ++++++++------ 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheEvictEvent.java index 16ce64256b..3bf7dce9ba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheEvictEvent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheEvictEvent.java @@ -15,15 +15,31 @@ */ package org.thingsboard.server.dao.ai; -import org.thingsboard.server.common.data.id.AiModelSettingsId; -import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.ai.AiModelSettings; -import java.util.Set; +import static java.util.Objects.requireNonNull; +import static org.thingsboard.server.dao.ai.AiModelSettingsCacheEvictEvent.Deleted; +import static org.thingsboard.server.dao.ai.AiModelSettingsCacheEvictEvent.Saved; -record AiModelSettingsCacheEvictEvent(Set keys) { +sealed interface AiModelSettingsCacheEvictEvent permits Saved, Deleted { + + AiModelSettingsCacheKey cacheKey(); + + record Saved(AiModelSettingsCacheKey cacheKey, AiModelSettings savedSettings) implements AiModelSettingsCacheEvictEvent { + + public Saved { + requireNonNull(cacheKey); + requireNonNull(savedSettings); + } + + } + + record Deleted(AiModelSettingsCacheKey cacheKey) implements AiModelSettingsCacheEvictEvent { + + public Deleted { + requireNonNull(cacheKey); + } - static AiModelSettingsCacheEvictEvent of(TenantId tenantId, AiModelSettingsId settingsId) { - return new AiModelSettingsCacheEvictEvent(Set.of(AiModelSettingsCacheKey.of(tenantId, settingsId))); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java index 411a67ca98..2dc3aba623 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.ai; -import com.google.common.collect.Sets; import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -38,7 +37,6 @@ import org.thingsboard.server.dao.sql.JpaExecutorService; import java.util.List; import java.util.Optional; -import java.util.Set; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @@ -54,7 +52,14 @@ class AiModelSettingsServiceImpl extends CachedVersionedEntityService cacheKeys = Sets.newHashSetWithExpectedSize(toDelete.size()); toDelete.forEach(settings -> { publishDeleteEvent(settings); - cacheKeys.add(AiModelSettingsCacheKey.of(settings.getTenantId(), settings.getId())); + publishEvictEvent(new AiModelSettingsCacheEvictEvent.Deleted(AiModelSettingsCacheKey.of(settings.getTenantId(), settings.getId()))); }); - - publishEvictEvent(new AiModelSettingsCacheEvictEvent(cacheKeys)); } private void publishDeleteEvent(AiModelSettings settings) { From cde8bd1c93f5075a08f25e9bc5ddf42ea20c06c1 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 1 Jul 2025 16:19:42 +0300 Subject: [PATCH 124/435] AI rule node: replace derived query for bulk deletion by tenant ID with JPQL query --- .../server/dao/sql/ai/AiModelSettingsRepository.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java index a86d5ba5c8..8bb432ca83 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java @@ -72,7 +72,9 @@ interface AiModelSettingsRepository extends JpaRepository ids); @Transactional - int deleteByTenantId(UUID tenantId); + @Modifying + @Query("DELETE FROM AiModelSettingsEntity ai_model WHERE ai_model.tenantId = :tenantId") + int deleteByTenantId(@Param("tenantId") UUID tenantId); @Transactional @Modifying From 8fb9e4968989bafa02dd9e1a0f040c12f69c51fb Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Tue, 1 Jul 2025 16:32:49 +0300 Subject: [PATCH 125/435] Style fix for custom action editor --- ...custom-action-pretty-editor.component.scss | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss index 002ccbeeb3..6db246723f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss @@ -19,6 +19,27 @@ padding: 8px; background-color: #fff; + .tb-custom-action-editor-container { + border: none; + } + + .tb-js-func-toolbar{ + padding: 0 3px; + } + + .tb-js-func { + &:not(.tb-fullscreen) { + &.tb-hide-brackets { + padding-bottom: 0; + } + } + + .tb-js-func-panel { + border: none; + border-top: 1px solid #c0c0c0; + } + } + .tb-fullscreen-panel { .tb-custom-action-editor-container { height: calc(100% - 40px); @@ -36,14 +57,46 @@ .mat-mdc-tab-group { .mat-mdc-tab-body-wrapper { height: 100%; + .mat-mdc-tab-body { height: 100%; + & > div { height: 100%; } } } } + + .right-panel{ + padding: 8px 0 25px; + } + + .tb-custom-action-editor-container { + border: none; + border-bottom: 1px solid #c0c0c0; + } + + .tb-js-func { + &.fill-height { + .tb-js-func-toolbar{ + padding: 0 5px; + } + &:not(.tb-fullscreen) { + &.tb-hide-brackets { + padding-bottom: 15px; + } + } + + &.tb-hide-brackets { + .tb-js-func-panel { + border: none; + border-top: 1px solid #c0c0c0; + border-bottom: 1px solid #c0c0c0; + } + } + } + } } .tb-split { From 41257b6138d706d545cfd7ad9cbf0f715cf27f1c Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 1 Jul 2025 16:58:24 +0300 Subject: [PATCH 126/435] AI rule node: optimize bulk deletion by tenant ID; remove application events --- .../entitiy/EntityStateSourcingListener.java | 4 +- .../queue/DefaultTbClusterService.java | 1 - .../server/dao/ai/AiModelSettingsDao.java | 3 +- .../dao/ai/AiModelSettingsServiceImpl.java | 41 ++----------------- .../dao/sql/ai/AiModelSettingsRepository.java | 9 +++- .../dao/sql/ai/JpaAiModelSettingsDao.java | 8 +++- 6 files changed, 21 insertions(+), 45 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index 5843d59315..03ab77ac09 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -99,7 +99,7 @@ public class EntityStateSourcingListener { case ASSET -> { onAssetUpdate(event.getEntity(), event.getOldEntity()); } - case ASSET_PROFILE, ENTITY_VIEW, NOTIFICATION_RULE, AI_MODEL_SETTINGS -> { + case ASSET_PROFILE, ENTITY_VIEW, NOTIFICATION_RULE -> { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, lifecycleEvent); } case RULE_CHAIN -> { @@ -164,7 +164,7 @@ public class EntityStateSourcingListener { Asset asset = (Asset) event.getEntity(); tbClusterService.onAssetDeleted(tenantId, asset, null); } - case ASSET_PROFILE, ENTITY_VIEW, CUSTOMER, EDGE, NOTIFICATION_RULE, AI_MODEL_SETTINGS -> { + case ASSET_PROFILE, ENTITY_VIEW, CUSTOMER, EDGE, NOTIFICATION_RULE -> { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, ComponentLifecycleEvent.DELETED); } case NOTIFICATION_REQUEST -> { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 3e509bd506..265f14c4e2 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -589,7 +589,6 @@ public class DefaultTbClusterService implements TbClusterService { EntityType.ENTITY_VIEW, EntityType.NOTIFICATION_RULE, EntityType.CALCULATED_FIELD, - EntityType.AI_MODEL_SETTINGS, EntityType.TENANT_PROFILE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE, diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java index 5ee0d49121..c8d6b65369 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java @@ -22,6 +22,7 @@ import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.TenantEntityDao; import java.util.Optional; +import java.util.Set; public interface AiModelSettingsDao extends TenantEntityDao, ExportableEntityDao { @@ -29,7 +30,7 @@ public interface AiModelSettingsDao extends TenantEntityDao, Ex boolean deleteById(TenantId tenantId, AiModelSettingsId settingsId); - int deleteByTenantId(TenantId tenantId); + Set deleteByTenantId(TenantId tenantId); boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java index 2dc3aba623..2494ab6a15 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java @@ -29,14 +29,12 @@ 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.eventsourcing.DeleteEntityEvent; -import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; 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.List; import java.util.Optional; +import java.util.Set; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @@ -65,7 +63,7 @@ class AiModelSettingsServiceImpl extends CachedVersionedEntityService toDeleteOpt = aiModelSettingsDao.findByTenantIdAndId(tenantId, settingsId); - if (toDeleteOpt.isEmpty()) { - return false; - } boolean deleted = aiModelSettingsDao.deleteByTenantIdAndId(tenantId, settingsId); if (deleted) { - publishDeleteEvent(toDeleteOpt.get()); publishEvictEvent(new AiModelSettingsCacheEvictEvent.Deleted(AiModelSettingsCacheKey.of(tenantId, settingsId))); } return deleted; @@ -153,25 +137,8 @@ class AiModelSettingsServiceImpl extends CachedVersionedEntityService toDelete = aiModelSettingsDao.findAllByTenantId(tenantId, new PageLink(Integer.MAX_VALUE)).getData(); - if (toDelete.isEmpty()) { - return; - } - - aiModelSettingsDao.deleteByTenantId(tenantId); - - toDelete.forEach(settings -> { - publishDeleteEvent(settings); - publishEvictEvent(new AiModelSettingsCacheEvictEvent.Deleted(AiModelSettingsCacheKey.of(settings.getTenantId(), settings.getId()))); - }); - } - - private void publishDeleteEvent(AiModelSettings settings) { - eventPublisher.publishEvent(DeleteEntityEvent.builder() - .tenantId(settings.getTenantId()) - .entityId(settings.getId()) - .entity(settings) - .build()); + Set deleted = aiModelSettingsDao.deleteByTenantId(tenantId); + deleted.forEach(id -> publishEvictEvent(new AiModelSettingsCacheEvictEvent.Deleted(AiModelSettingsCacheKey.of(tenantId, id)))); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java index 8bb432ca83..aae01a3b41 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java @@ -73,8 +73,13 @@ interface AiModelSettingsRepository extends JpaRepository deleteByTenantId(@Param("tenantId") UUID tenantId); @Transactional @Modifying diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java index a09179d31e..38e7280313 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java @@ -39,6 +39,8 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import static java.util.stream.Collectors.toSet; + @SqlDao @Component @RequiredArgsConstructor @@ -108,8 +110,10 @@ class JpaAiModelSettingsDao extends JpaAbstractDao deleteByTenantId(TenantId tenantId) { + return aiModelSettingsRepository.deleteByTenantId(tenantId.getId()).stream() + .map(AiModelSettingsId::new) + .collect(toSet()); } @Override From 17a534abf4bc7a52838f5ffe86d29cdfe8bf35fb Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 1 Jul 2025 18:10:17 +0300 Subject: [PATCH 127/435] AI rule node: remove redundant number of entities validation --- .../dao/service/validator/AiModelSettingsDataValidator.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java index 6ed84e3c3e..726fb6331f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java @@ -17,7 +17,6 @@ package org.thingsboard.server.dao.service.validator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.ai.AiModelSettingsDao; @@ -34,11 +33,6 @@ class AiModelSettingsDataValidator extends DataValidator { private final TenantService tenantService; private final AiModelSettingsDao aiModelSettingsDao; - @Override - protected void validateCreate(TenantId tenantId, AiModelSettings settings) { - validateNumberOfEntitiesPerTenant(tenantId, EntityType.AI_MODEL_SETTINGS); - } - @Override protected AiModelSettings validateUpdate(TenantId tenantId, AiModelSettings settings) { Optional existing = aiModelSettingsDao.findByTenantIdAndId(tenantId, settings.getId()); From 3ef97cc2096496c5c9a2e17ed047f6d870e5f6fb Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 1 Jul 2025 18:10:47 +0300 Subject: [PATCH 128/435] AI rule node: ensure "null" UUIDs are not accepted by AI model settings cache key --- .../server/dao/ai/AiModelSettingsCacheKey.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheKey.java index 9d73289be7..60d0ccaee9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheKey.java @@ -18,6 +18,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.EntityId; import org.thingsboard.server.common.data.id.TenantId; import java.util.UUID; @@ -29,6 +30,13 @@ record AiModelSettingsCacheKey(UUID tenantId, UUID settingsId) implements Versio AiModelSettingsCacheKey { requireNonNull(tenantId); requireNonNull(settingsId); + + 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"); + } } static AiModelSettingsCacheKey of(TenantId tenantId, AiModelSettingsId settingsId) { From c0b44a95b276f006e1f9c65ae0116ccaaa495804 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 1 Jul 2025 21:04:56 +0300 Subject: [PATCH 129/435] AI rule node: change URL for AI model settings API to be more RESTful --- .../server/controller/AiModelSettingsController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java index 9659be1941..178d7017a0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java @@ -46,7 +46,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERT import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; @RestController -@RequestMapping("/api/ai-model-settings") +@RequestMapping("/api/ai/model/settings") class AiModelSettingsController extends BaseController { @ApiOperation( From d17f23b7efed689290cc3366ea9208ab7ca602b4 Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Wed, 2 Jul 2025 12:30:45 +0300 Subject: [PATCH 130/435] Add 4.0.2 as a supported version for upgrade --- .../service/install/DefaultDatabaseSchemaSettingsService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java index 90c5966f3f..ad87242c0a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java @@ -32,7 +32,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti // This list should include all versions which are compatible for the upgrade. // The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release. - private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.0.0", "4.0.1"); + private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.0.0", "4.0.1", "4.0.2"); private final ProjectInfo projectInfo; private final JdbcTemplate jdbcTemplate; From fb966bded0fbdadf5ebd43d7afddb0278cf36bfb Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Wed, 2 Jul 2025 12:46:50 +0300 Subject: [PATCH 131/435] Fix no type-cast for attributes; telemetry controller refactoring --- .../controller/TelemetryController.java | 133 +++++++----------- .../telemetry/BaseTelemetryProcessor.java | 4 +- .../csv/AbstractBulkImportService.java | 3 +- .../server/common/adaptor/JsonConverter.java | 8 +- .../common/adaptor/JsonConverterTest.java | 5 +- .../TbCopyAttributesToEntityViewNode.java | 8 +- .../rule/engine/profile/DeviceState.java | 4 +- .../telemetry/TbCalculatedFieldsNode.java | 3 +- .../engine/telemetry/TbMsgAttributesNode.java | 3 +- 9 files changed, 63 insertions(+), 108 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index bf9713f58e..c98ae0dcb8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -33,11 +33,11 @@ import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; 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.RequestMethod; @@ -65,25 +65,17 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; -import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; -import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.IntervalType; -import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; -import org.thingsboard.server.common.data.kv.LongDataEntry; -import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.exception.InvalidParametersException; -import org.thingsboard.server.exception.UncheckedApiException; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.AccessValidator; import org.thingsboard.server.service.security.model.SecurityUser; @@ -156,9 +148,6 @@ public class TelemetryController extends BaseController { @Autowired private TbTelemetryService tbTelemetryService; - @Value("${transport.json.max_string_value_length:0}") - private int maxStringValueLength; - private ExecutorService executor; @PostConstruct @@ -314,10 +303,10 @@ public class TelemetryController extends BaseController { @Parameter(description = "A string value representing the timezone that will be used to calculate exact timestamps for 'WEEK', 'WEEK_ISO', 'MONTH' and 'QUARTER' interval types.") @RequestParam(name = "timeZone", required = false) String timeZone, @Parameter(description = "An integer value that represents a max number of time series data points to fetch." + - " This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", schema = @Schema(defaultValue = "100")) + " This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", schema = @Schema(defaultValue = "100")) @RequestParam(name = "limit", defaultValue = "100") Integer limit, @Parameter(description = "A string value representing the aggregation function. " + - "If the interval is not specified, 'agg' parameter will use 'NONE' value.", + "If the interval is not specified, 'agg' parameter will use 'NONE' value.", schema = @Schema(allowableValues = {"MIN", "MAX", "AVG", "SUM", "COUNT", "NONE"})) @RequestParam(name = "agg", defaultValue = "NONE") String aggStr, @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @@ -337,20 +326,21 @@ public class TelemetryController extends BaseController { + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = SAVE_ATTIRIBUTES_STATUS_OK + - "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED', " + - "and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'."), + "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED', " + + "and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'."), @ApiResponse(responseCode = "400", description = SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST), @ApiResponse(responseCode = "401", description = "User is not authorized to save device attributes for selected device. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + - "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace."), + "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace."), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.POST) - @ResponseBody - public DeferredResult saveDeviceAttributes( - @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("deviceId") String deviceIdStr, - @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { + @PostMapping(value = "/{deviceId}/{scope}") + public DeferredResult saveDeviceAttributes(@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("deviceId") String deviceIdStr, + @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) + @PathVariable("scope") AttributeScope scope, + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) + @RequestBody String request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); return saveAttributes(getTenantId(), entityId, scope, request); } @@ -367,13 +357,15 @@ public class TelemetryController extends BaseController { @ApiResponse(responseCode = "500", description = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.POST) - @ResponseBody - public DeferredResult saveEntityAttributesV1( - @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, - @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"})) @PathVariable("scope") AttributeScope scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { + @PostMapping(value = "/{entityType}/{entityId}/{scope}") + public DeferredResult saveEntityAttributesV1(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) + @PathVariable("entityType") String entityType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("entityId") String entityIdStr, + @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"})) + @PathVariable("scope") AttributeScope scope, + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) + @RequestBody String request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveAttributes(getTenantId(), entityId, scope, request); } @@ -390,13 +382,15 @@ public class TelemetryController extends BaseController { @ApiResponse(responseCode = "500", description = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{entityType}/{entityId}/attributes/{scope}", method = RequestMethod.POST) - @ResponseBody - public DeferredResult saveEntityAttributesV2( - @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, - @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { + @PostMapping(value = "/{entityType}/{entityId}/attributes/{scope}") + public DeferredResult saveEntityAttributesV2(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) + @PathVariable("entityType") String entityType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("entityId") String entityIdStr, + @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) + @PathVariable("scope") AttributeScope scope, + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) + @RequestBody String request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveAttributes(getTenantId(), entityId, scope, request); } @@ -460,11 +454,11 @@ public class TelemetryController extends BaseController { TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Time series for the selected keys in the request was removed. " + - "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."), + "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys list is empty or start and end timestamp values is empty when deleteAllDataForKeys is set to false."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete entity time series for selected entity. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + - "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."), + "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/timeseries/delete", method = RequestMethod.DELETE) @@ -541,11 +535,11 @@ public class TelemetryController extends BaseController { "Referencing a non-existing Device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Device attributes was removed for the selected keys in the request. " + - "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."), + "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete device attributes for selected entity. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + - "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), + "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE) @@ -563,11 +557,11 @@ public class TelemetryController extends BaseController { INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Entity attributes was removed for the selected keys in the request. " + - "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."), + "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + - "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), + "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE) @@ -616,18 +610,24 @@ public class TelemetryController extends BaseController { }); } - private DeferredResult saveAttributes(TenantId srcTenantId, EntityId entityIdSrc, AttributeScope scope, JsonNode json) throws ThingsboardException { + private DeferredResult saveAttributes(TenantId srcTenantId, EntityId entityIdSrc, AttributeScope scope, String jsonStr) throws ThingsboardException { if (AttributeScope.SERVER_SCOPE != scope && AttributeScope.SHARED_SCOPE != scope) { return getImmediateDeferredResult("Invalid scope: " + scope, HttpStatus.BAD_REQUEST); } - if (json.isObject()) { - List attributes = extractRequestAttributes(json); + JsonElement json; + try { + json = JsonParser.parseString(jsonStr); + } catch (Exception e) { + return getImmediateDeferredResult("Invalid JSON", HttpStatus.BAD_REQUEST); + } + if (json.isJsonObject()) { + List attributes = JsonConverter.convertToAttributes(json); if (attributes.isEmpty()) { return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST); } for (AttributeKvEntry attributeKvEntry : attributes) { - if (attributeKvEntry.getKey().isEmpty() || attributeKvEntry.getKey().trim().length() == 0) { - return getImmediateDeferredResult("Key cannot be empty or contains only spaces", HttpStatus.BAD_REQUEST); + if (attributeKvEntry.getKey().isBlank()) { + return getImmediateDeferredResult("Key cannot be blank", HttpStatus.BAD_REQUEST); } } SecurityUser user = getCurrentUser(); @@ -885,43 +885,6 @@ public class TelemetryController extends BaseController { return result; } - private List extractRequestAttributes(JsonNode jsonNode) { - long ts = System.currentTimeMillis(); - List attributes = new ArrayList<>(); - jsonNode.fields().forEachRemaining(entry -> { - String key = entry.getKey(); - JsonNode value = entry.getValue(); - if (entry.getValue().isObject() || entry.getValue().isArray()) { - attributes.add(new BaseAttributeKvEntry(new JsonDataEntry(key, toJsonStr(value)), ts)); - } else if (entry.getValue().isTextual()) { - if (maxStringValueLength > 0 && entry.getValue().textValue().length() > maxStringValueLength) { - String message = String.format("String value length [%d] for key [%s] is greater than maximum allowed [%d]", entry.getValue().textValue().length(), key, maxStringValueLength); - throw new UncheckedApiException(new InvalidParametersException(message)); - } - attributes.add(new BaseAttributeKvEntry(new StringDataEntry(key, value.textValue()), ts)); - } else if (entry.getValue().isBoolean()) { - attributes.add(new BaseAttributeKvEntry(new BooleanDataEntry(key, value.booleanValue()), ts)); - } else if (entry.getValue().isDouble()) { - attributes.add(new BaseAttributeKvEntry(new DoubleDataEntry(key, value.doubleValue()), ts)); - } else if (entry.getValue().isNumber()) { - if (entry.getValue().isBigInteger()) { - throw new UncheckedApiException(new InvalidParametersException("Big integer values are not supported!")); - } else { - attributes.add(new BaseAttributeKvEntry(new LongDataEntry(key, value.longValue()), ts)); - } - } - }); - return attributes; - } - - private String toJsonStr(JsonNode value) { - try { - return JacksonUtil.toString(value); - } catch (IllegalArgumentException e) { - throw new JsonParseException("Can't parse jsonValue: " + value, e); - } - } - private JsonNode toJsonNode(String value) { try { return JacksonUtil.toJsonNode(value); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java index eeaa80d6a2..068af492bc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java @@ -266,7 +266,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { SettableFuture futureToSet = SettableFuture.create(); JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); AttributeScope scope = AttributeScope.valueOf(metaData.getValue(DataConstants.SCOPE)); - List attributes = new ArrayList<>(JsonConverter.convertToAttributes(json, ts)); + List attributes = JsonConverter.convertToAttributes(json, ts); ListenableFuture> future = filterAttributesByTs(tenantId, entityId, scope, attributes); Futures.addCallback(future, new FutureCallback<>() { @Override @@ -314,7 +314,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { SettableFuture futureToSet = SettableFuture.create(); JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); AttributeScope scope = AttributeScope.valueOf(metaData.getValue(DataConstants.SCOPE)); - List attributes = new ArrayList<>(JsonConverter.convertToAttributes(json, ts)); + List attributes = JsonConverter.convertToAttributes(json, ts); ListenableFuture> future = filterAttributesByTs(tenantId, entityId, scope, attributes); Futures.addCallback(future, new FutureCallback<>() { @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java index 3ea50ea327..9850e2d1a1 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java @@ -67,7 +67,6 @@ import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import org.thingsboard.server.utils.CsvUtils; -import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; @@ -235,7 +234,7 @@ public abstract class AbstractBulkImportService kvsEntry, BulkImportColumnType kvType) { String scope = kvType.getKey(); - List attributes = new ArrayList<>(JsonConverter.convertToAttributes(kvsEntry.getValue())); + List attributes = JsonConverter.convertToAttributes(kvsEntry.getValue()); accessValidator.validateEntityAndCallback(user, Operation.WRITE_ATTRIBUTES, entity.getId(), (result, tenantId, entityId) -> { tsSubscriptionService.saveAttributes(AttributesSaveRequest.builder() diff --git a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java index 2a208923d9..6518cfc3fe 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java @@ -56,11 +56,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509Ce import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Set; import java.util.TreeMap; import java.util.function.Consumer; @@ -538,13 +536,13 @@ public class JsonConverter { return result; } - public static Set convertToAttributes(JsonElement element) { + public static List convertToAttributes(JsonElement element) { long ts = System.currentTimeMillis(); return convertToAttributes(element, ts); } - public static Set convertToAttributes(JsonElement element, long ts) { - return new HashSet<>(parseValues(element.getAsJsonObject()).stream().map(kv -> new BaseAttributeKvEntry(kv, ts)).toList()); + public static List convertToAttributes(JsonElement element, long ts) { + return parseValues(element.getAsJsonObject()).stream().map(kv -> new BaseAttributeKvEntry(kv, ts)).toList(); } private static List parseValues(JsonObject valuesObject) { diff --git a/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java b/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java index 28b65a7be8..38ef604ab0 100644 --- a/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java +++ b/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java @@ -23,8 +23,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Isolated; -import java.util.ArrayList; - @Isolated("JsonConverter static settings being modified") public class JsonConverterTest { @@ -53,7 +51,7 @@ public class JsonConverterTest { @Test public void testParseAttributesBigDecimalAsLong() { - var result = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString("{\"meterReadingDelta\": 1E1}"))); + var result = JsonConverter.convertToAttributes(JsonParser.parseString("{\"meterReadingDelta\": 1E1}")); Assertions.assertEquals(10L, result.get(0).getLongValue().get().longValue()); } @@ -108,4 +106,5 @@ public class JsonConverterTest { JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L); }); } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java index 7e3c08cfcc..2d74929242 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java @@ -44,7 +44,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.ArrayList; import java.util.List; -import java.util.Set; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT; @@ -115,14 +114,13 @@ public class TbCopyAttributesToEntityViewNode implements TbNode { .build()); } } else { - Set attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())); - List filteredAttributes = - attributes.stream().filter(attr -> attributeContainsInEntityView(scope, attr.getKey(), entityView)).collect(Collectors.toList()); + List attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())).stream() + .filter(attr -> attributeContainsInEntityView(scope, attr.getKey(), entityView)).toList(); ctx.getTelemetryService().saveAttributes(AttributesSaveRequest.builder() .tenantId(ctx.getTenantId()) .entityId(entityView.getId()) .scope(scope) - .entries(filteredAttributes) + .entries(attributes) .callback(getFutureCallback(ctx, msg, entityView)) .build()); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java index 193c179df2..4bd81050db 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java @@ -258,7 +258,7 @@ class DeviceState { private boolean processAttributes(TbContext ctx, TbMsg msg, String scope) throws ExecutionException, InterruptedException { boolean stateChanged = false; - Set attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())); + List attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())); if (!attributes.isEmpty()) { SnapshotUpdate update = merge(latestValues, attributes, scope); for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { @@ -321,7 +321,7 @@ class DeviceState { return new SnapshotUpdate(AlarmConditionKeyType.TIME_SERIES, keys); } - private SnapshotUpdate merge(DataSnapshot latestValues, Set attributes, String scope) { + private SnapshotUpdate merge(DataSnapshot latestValues, List attributes, String scope) { long newTs = 0; Set keys = new HashSet<>(); for (AttributeKvEntry entry : attributes) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java index 4e319500d1..e703e9dd25 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java @@ -103,8 +103,7 @@ public class TbCalculatedFieldsNode implements TbNode { } private void processPostAttributesRequest(TbContext ctx, TbMsg msg) { - List newAttributes = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData()))); - + List newAttributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())); if (newAttributes.isEmpty()) { ctx.tellSuccess(msg); return; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java index c04f5b474d..280d8de824 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java @@ -41,7 +41,6 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -133,7 +132,7 @@ public class TbMsgAttributesNode implements TbNode { return; } String src = msg.getData(); - List newAttributes = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString(src))); + List newAttributes = JsonConverter.convertToAttributes(JsonParser.parseString(src)); if (newAttributes.isEmpty()) { ctx.tellSuccess(msg); return; From a5d9fe8751e38a61be6c4854b8920d6238e280c3 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 2 Jul 2025 13:39:42 +0300 Subject: [PATCH 132/435] UI: Improved help popup container style --- ui-ngx/src/app/shared/components/help-popup.component.html | 2 +- ui-ngx/src/app/shared/components/help-popup.component.scss | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/shared/components/help-popup.component.html b/ui-ngx/src/app/shared/components/help-popup.component.html index c24b5234b9..c4a66ed452 100644 --- a/ui-ngx/src/app/shared/components/help-popup.component.html +++ b/ui-ngx/src/app/shared/components/help-popup.component.html @@ -34,7 +34,7 @@ -
+
-
mobile.configuration-step.configure-api-title
-
mobile.configuration-step.configure-api-text
- -
mobile.configuration-step.configure-api-hint
- -
-
-
mobile.configuration-step.configure-package-title
-
mobile.configuration-step.configure-package-text
-
mobile.configuration-step.configure-package-text-install
- -
mobile.configuration-step.configure-package-run-commands
- +
mobile.configuration-step.configure-app-settings-title
+
+
mobile.configuration-step.configure-app-settings-text
+ +
mobile.configuration-step.run-app-title
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.ts index 9168f14245..5e7d384383 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.ts @@ -22,6 +22,7 @@ import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { ActionPreferencesPutUserSettings } from '@core/auth/auth.actions'; import { MobileApp } from '@shared/models/mobile-app.models'; +import { ImportExportService } from '@shared/import-export/import-export.service'; export interface MobileAppConfigurationDialogData { afterAdd: boolean; @@ -36,53 +37,22 @@ export interface MobileAppConfigurationDialogData { }) export class MobileAppConfigurationDialogComponent extends DialogComponent { - notShowAgain = false; - setApplication = false; + private fileName = 'configs.json'; + notShowAgain = false; showDontShowAgain: boolean; gitRepositoryLink = 'git clone -b master https://github.com/thingsboard/flutter_thingsboard_app.git'; - pathToConstants = 'lib/constants/app_constants.dart'; - flutterRunCommand = 'flutter run'; - flutterInstallRenameCommand = 'flutter pub global activate rename'; - - configureApi: string; - - renameCommands: string[] = []; + flutterRunCommand = `flutter run --dart-define-from-file ${this.fileName}`; constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) private data: MobileAppConfigurationDialogData, protected dialogRef: MatDialogRef, + private importExportService: ImportExportService, ) { super(store, router, dialogRef); - this.showDontShowAgain = this.data.afterAdd; - - this.setApplication = !!this.data.androidApp || !!this.data.iosApp; - - this.configureApi = `static const thingsBoardApiEndpoint = '${window.location.origin}';`; - if (this.setApplication) { - this.configureApi += '\n'; - if (!!this.data.androidApp) { - this.configureApi += `\nstatic const thingsboardAndroidAppSecret = '${this.data.androidApp.appSecret}';`; - } - if (!!this.data.iosApp) { - this.configureApi += `\nstatic const thingsboardIOSAppSecret = '${this.data.iosApp.appSecret}';`; - } - } - if (this.setApplication) { - if (this.data.androidApp?.pkgName === this.data.iosApp?.pkgName) { - this.renameCommands.push(`rename setBundleId --targets android, ios --value "${this.data.androidApp.pkgName}"`); - } else { - if (!!this.data.androidApp) { - this.renameCommands.push(`rename setBundleId --targets android --value "${this.data.androidApp.pkgName}"`); - } - if (!!this.data.iosApp) { - this.renameCommands.push(`rename setBundleId --targets ios --value "${this.data.iosApp.pkgName}"`); - } - } - } } close(): void { @@ -94,14 +64,24 @@ export class MobileAppConfigurationDialogComponent extends DialogComponent = []; - commands.forEach(command => formatCommands.push(this.createMarkDownSingleCommand(command))); - return formatCommands.join(`\n
\n\n`); - } else { - return this.createMarkDownSingleCommand(commands); + createMarkDownCommand(commands: string): string { + return this.createMarkDownSingleCommand(commands); + } + + downloadSettings(): void { + const settings: any = { + thingsBoardApiEndpoint: window.location.origin + }; + if (!!this.data.androidApp) { + settings.androidApplicationId = this.data.androidApp.pkgName; + settings.thingsboardOAuth2CallbackUrlScheme = this.data.androidApp.pkgName + '.auth'; + settings.thingsboardAndroidAppSecret = this.data.androidApp.appSecret; + } + if (!!this.data.iosApp) { + settings.iosApplicationId = this.data.iosApp.pkgName; + settings.thingsboardIOSAppSecret = this.data.iosApp.appSecret; } + this.importExportService.exportJson(settings, this.fileName); } private createMarkDownSingleCommand(command: string): string { diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index 2ad60f0139..94960cb838 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -1188,7 +1188,7 @@ export class ImportExportService { this.exportJson(data, filename); } - private exportJson(data: any, filename: string) { + public exportJson(data: any, filename: string) { if (isObject(data)) { data = JSON.stringify(data, null, 2); } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 963bf769ee..14395f3b95 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3850,17 +3850,13 @@ "prepare-environment-text": "Flutter ThingsBoard Mobile Application requires Flutter SDK. Follow instructions to set up Flutter SDK.", "get-source-code-title": "Get app source code", "get-source-code-text": "You can get Flutter ThingsBoard Mobile Application source code by cloning it from the GitHub repository:", - "configure-api-title": "Configure ThingsBoard API endpoint", - "configure-api-text": "Open the flutter_thingsboard_app project in your editor/IDE. Edit:", - "configure-api-hint": "Set the value of the thingsBoardApiEndpoint constant to match the API endpoint of your ThingsBoard server instance. Do not use “localhost” or “127.0.0.1” hostnames.", + "configure-app-settings-title": "Configure app settings", + "configure-app-settings-text": "Download the configuration file and place it into the root directory of the project you cloned in the previous step.", + "download-file": "Download file", "run-app-title": "Run the app", "run-app-text": "Run the app as described in your IDE.\nIf using the terminal, run the app with the following command:", "more-information": "Detailed information may be found in our Getting Started documentation.", - "getting-started": "Getting Started", - "configure-package-title": "Configure application package", - "configure-package-text": "You can manually change the Application Package or use third party CLI tool.", - "configure-package-text-install": "To install the Rename CLI Tool, execute the following command:", - "configure-package-run-commands": "Run these commands in the root directory of your project:" + "getting-started": "Getting Started" } }, "notification": { From f59b6c95e48d13cc2c91f12ca447457417a2eb60 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 4 Jul 2025 13:40:44 +0300 Subject: [PATCH 145/435] updated tests for geofencing unility methods --- .../server/service/script/TbelInvokeDocsIoTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index a90f9f19b0..c283a791fd 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -2451,7 +2451,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { public void isInsidePolygon_Test() throws ExecutionException, InterruptedException { msgStr = "{}"; decoderStr = """ - String perimeter = "[[[37.7810,-122.4210],[37.7890,-122.3900],[37.7700,-122.3800],[37.7600,-122.4000],[37.7700,-122.4250],[37.7810,-122.4210]],[[37.7730,-122.4050],[37.7700,-122.3950],[37.7670,-122.3980],[37.7690,-122.4100],[37.7730,-122.4050]]]"; + var perimeter = "[[[37.7810,-122.4210],[37.7890,-122.3900],[37.7700,-122.3800],[37.7600,-122.4000],[37.7700,-122.4250],[37.7810,-122.4210]],[[37.7730,-122.4050],[37.7700,-122.3950],[37.7670,-122.3980],[37.7690,-122.4100],[37.7730,-122.4050]]]"; return{ outsidePolygon: isInsidePolygon(37.8000, -122.4300, perimeter), insidePolygon: isInsidePolygon(37.7725, -122.4010, perimeter), @@ -2470,7 +2470,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { public void isInsideCircle_Test() throws ExecutionException, InterruptedException { msgStr = "{}"; decoderStr = """ - String perimeter = "{\\"latitude\\":37.7749,\\"longitude\\":-122.4194,\\"radius\\":3000,\\"radiusUnit\\":\\"METER\\"}"; + var perimeter = "{\\"latitude\\":37.7749,\\"longitude\\":-122.4194,\\"radius\\":3000,\\"radiusUnit\\":\\"METER\\"}"; return{ outsideCircle: isInsideCircle(37.8044, -122.2712, perimeter), insideCircle: isInsideCircle(37.7599, -122.4148, perimeter) From ad6e29145a88434b8666406f18838d332a1d0011 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 4 Jul 2025 12:45:28 +0300 Subject: [PATCH 146/435] UI: Fix rewrite of dashboard duplicate state with same id. --- .../states/manage-dashboard-states-dialog.component.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts index 107d5c2686..7dddc18dbd 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts @@ -261,11 +261,16 @@ export class ManageDashboardStatesDialogComponent const originalState = state; const newStateName = this.getNextDuplicatedName(state.name); if (newStateName) { + const newStateId = newStateName.toLowerCase().replace(/\W/g, '_'); + if (this.states[newStateId]) { + this.stateNames.add(newStateName); + this.duplicateState(null, state); + } const duplicatedStates = deepClone(originalState); const duplicatedWidgets = deepClone(this.widgets); const mainWidgets = {}; const rightWidgets = {}; - duplicatedStates.id = newStateName.toLowerCase().replace(/\W/g, '_'); + duplicatedStates.id = newStateId; duplicatedStates.name = newStateName; duplicatedStates.root = false; this.stateNames.add(duplicatedStates.name); From 8415e91e98ef4ce851b370e94f7de2c4ad28f8a0 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 4 Jul 2025 12:37:55 +0300 Subject: [PATCH 147/435] UI: OTA package form fix scrolling for long file name. --- .../app/modules/home/pages/ota-update/ota-update.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html index e028a2714a..ee8e34a4a2 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html @@ -63,7 +63,7 @@
-
+
ota-update.title From 075429fe87cf85fafeadf06786ffd8f739cad54d Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 4 Jul 2025 12:27:57 +0300 Subject: [PATCH 148/435] UI: Mobile center update app-id translations. --- ui-ngx/src/assets/locale/locale.constant-en_US.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 963bf769ee..4d3f53f460 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3731,9 +3731,9 @@ }, "mobile": { "add-application": "Add application", - "app-id": "App ID", - "app-id-required": "App ID is required", - "app-id-pattern": "Invalid format App ID", + "app-id": "App Site Association ID", + "app-id-required": "App Site Association ID is required", + "app-id-pattern": "Invalid format App Site Association ID", "app-store-link": "App Store link", "app-store-link-required": "App Store link is required", "application-details": "Application details", From cdb7e3b0c03bb07ced7a3a078383fbd32d5bcbc5 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 4 Jul 2025 14:52:19 +0300 Subject: [PATCH 149/435] AI rule node: correct method name for schema adapter --- .../rule/engine/ai/Langchain4jJsonSchemaAdapter.java | 2 +- .../src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java index e136bfaea8..b04d4592a2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java @@ -46,7 +46,7 @@ final class Langchain4jJsonSchemaAdapter { * @param rootSchemaNode a valid JSON Schema as a Jackson {@link ObjectNode} * @return the corresponding Langchain4j {@link JsonSchema} */ - public static JsonSchema fromJsonNode(ObjectNode rootSchemaNode) { + public static JsonSchema fromObjectNode(ObjectNode rootSchemaNode) { return JsonSchema.builder() .name(rootSchemaNode.get("title").textValue()) .rootElement(parse(rootSchemaNode)) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 9e7d3fb23b..fe05712bea 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -84,7 +84,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { if (config.getResponseFormatType() == ResponseFormatType.JSON) { responseFormat = ResponseFormat.builder() .type(config.getResponseFormatType()) - .jsonSchema(config.getJsonSchema() != null ? Langchain4jJsonSchemaAdapter.fromJsonNode(config.getJsonSchema()) : null) + .jsonSchema(config.getJsonSchema() != null ? Langchain4jJsonSchemaAdapter.fromObjectNode(config.getJsonSchema()) : null) .build(); } From 749b327795f3b2e26aa7f2afe3e34a958b415a7b Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 4 Jul 2025 20:21:29 +0300 Subject: [PATCH 150/435] AI rule node: resolve dependency conflicts --- application/pom.xml | 6 +++ common/data/pom.xml | 2 +- pom.xml | 104 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index 543724fa10..e2faa21929 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -412,6 +412,12 @@ dev.langchain4j langchain4j-github-models + + + com.azure + azure-core-test + + diff --git a/common/data/pom.xml b/common/data/pom.xml index f57af8d24b..f582c32023 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -114,7 +114,7 @@ dev.langchain4j - langchain4j + langchain4j-core diff --git a/pom.xml b/pom.xml index 349f09606c..edd15c778c 100755 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,8 @@ 4.1.119.Final 2.0.65.Final 1.1.18 + 1.0.4 + 3.6.12 1.7.1 5.21.0 3.2.5 @@ -119,7 +121,14 @@ 2.2 1.12.701 1.128.1 - 2.37.1 + + 1.34.0 + 1.46.3 + 2.48.0 + 2.65.0 + 2.37.1 + 1.51.0 + 1.6.4 1.6.1 1.9.4 @@ -136,6 +145,12 @@ 3.8.0 2.9.0 1.1.0 + 2.38.0 + 1.24 + 1.11.0 + 3.49.3 + 0.27.0 + 1.7.0 4.2.1 2.7.3 @@ -1261,6 +1276,16 @@ reactor-netty-http ${reactor-netty.version} + + org.reactivestreams + reactive-streams + ${reactive-streams.version} + + + io.projectreactor + reactor-core + ${reactor-core.version} + org.apache.kafka kafka-clients @@ -2080,10 +2105,55 @@ google-cloud-pubsub ${pubsub.client.version} + + com.google.auth + google-auth-library-credentials + ${google-auth-library.version} + + + com.google.auth + google-auth-library-oauth2-http + ${google-auth-library.version} + + + com.google.http-client + google-http-client + ${google-http-client.version} + + + com.google.http-client + google-http-client-gson + ${google-http-client.version} + + + com.google.api + api-common + ${google-api-common.version} + + + com.google.api + gax + ${google-api-gax.version} + + + com.google.api + gax-grpc + ${google-api-gax.version} + + + com.google.api + gax-httpjson + ${google-api-gax.version} + com.google.api.grpc proto-google-common-protos - ${google.common.protos.version} + ${google-proto-common.version} + + + com.google.api.grpc + proto-google-iam-v1 + ${google-proto-iam-v1.version} org.passay @@ -2337,6 +2407,36 @@ rocksdbjni ${rocksdbjni.version} + + com.google.errorprone + error_prone_annotations + ${error_prone_annotations.version} + + + org.codehaus.mojo + animal-sniffer-annotations + ${animal-sniffer-annotations.version} + + + com.google.auto.value + auto-value-annotations + ${auto-value-annotations.version} + + + org.checkerframework + checker-qual + ${checker-qual.version} + + + io.perfmark + perfmark-api + ${perfmark-api.version} + + + org.threeten + threetenbp + ${threetenbp.version} + dev.langchain4j langchain4j-bom From 83eef548bbf128cb69b7b510dd2a32caeedd1ed9 Mon Sep 17 00:00:00 2001 From: imbeacon Date: Mon, 7 Jul 2025 14:45:14 +0300 Subject: [PATCH 151/435] Fixed issue that leads to double counting for devices connected through gateway --- .../limits/DefaultTransportRateLimitService.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java index a5ac06bfb6..d03a3d03f2 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java @@ -82,16 +82,18 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi if (!checkEntityRateLimit(dataPoints, getTenantRateLimits(tenantId))) { return TbPair.of(EntityType.TENANT, false); } - if (isGateway && !checkEntityRateLimit(dataPoints, getGatewayDeviceRateLimits(tenantId, deviceId))) { - return TbPair.of(EntityType.DEVICE, true); + if (isGateway) { + if (!checkEntityRateLimit(dataPoints, getGatewayDeviceRateLimits(tenantId, deviceId))) { + return TbPair.of(EntityType.DEVICE, true); + } + } else if (gatewayId == null && deviceId != null) { + if (!checkEntityRateLimit(dataPoints, getDeviceRateLimits(tenantId, deviceId))) { + return TbPair.of(EntityType.DEVICE, false); + } } if (gatewayId != null && !checkEntityRateLimit(dataPoints, getGatewayRateLimits(tenantId, gatewayId))) { return TbPair.of(EntityType.DEVICE, true); } - if (!isGateway && deviceId != null && !checkEntityRateLimit(dataPoints, getDeviceRateLimits(tenantId, deviceId))) { - return TbPair.of(EntityType.DEVICE, false); - } - return null; } From 69964a2413933c8b97d4dafeb1746dee47d28429 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 7 Jul 2025 17:07:30 +0300 Subject: [PATCH 152/435] Make script compilation errors unrecoverable during rule node initialization --- .../service/script/RuleNodeScriptEngine.java | 8 ++++--- .../server/actors/TbActorMailbox.java | 5 +++-- .../script/api/TbScriptException.java | 21 +++++++++++++++++-- .../script/api/js/NashornJsInvokeService.java | 7 ++++++- .../api/tbel/DefaultTbelInvokeService.java | 16 ++++++++------ .../common/util/RecoveryAware.java} | 4 ++-- .../rule/engine/api/TbNodeException.java | 7 ++----- 7 files changed, 47 insertions(+), 21 deletions(-) rename common/{message/src/main/java/org/thingsboard/server/common/msg/TbActorError.java => util/src/main/java/org/thingsboard/common/util/RecoveryAware.java} (89%) diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java index 8f19aeb0a3..d99f1654f3 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java @@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.script.api.ScriptInvokeService; import org.thingsboard.script.api.ScriptType; +import org.thingsboard.script.api.TbScriptException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbMsg; @@ -32,7 +33,6 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; - @Slf4j public abstract class RuleNodeScriptEngine implements ScriptEngine { @@ -51,7 +51,10 @@ public abstract class RuleNodeScriptEngine imp if (e instanceof ExecutionException) { t = e.getCause(); } - throw new IllegalArgumentException("Can't compile script: " + t.getMessage(), t); + if (t instanceof TbScriptException scriptException) { + throw scriptException; + } + throw new RuntimeException("Unexpected error when creating script engine: " + t.getMessage(), t); } } @@ -81,7 +84,6 @@ public abstract class RuleNodeScriptEngine imp return Futures.transformAsync(executeScriptAsync(msg), this::executeToStringTransform, MoreExecutors.directExecutor()); } - @Override public ListenableFuture executeFilterAsync(TbMsg msg) { return Futures.transformAsync(executeScriptAsync(msg), diff --git a/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java b/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java index 6cd28fa98d..c20726d765 100644 --- a/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java +++ b/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java @@ -19,8 +19,8 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.common.util.RecoveryAware; import org.thingsboard.server.common.msg.MsgType; -import org.thingsboard.server.common.msg.TbActorError; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbActorStopReason; @@ -35,6 +35,7 @@ import java.util.function.Supplier; @Getter @RequiredArgsConstructor public final class TbActorMailbox implements TbActorCtx { + private static final boolean HIGH_PRIORITY = true; private static final boolean NORMAL_PRIORITY = false; @@ -100,7 +101,7 @@ public final class TbActorMailbox implements TbActorCtx { if (t instanceof TbActorException && t.getCause() != null) { t = t.getCause(); } - return t instanceof TbActorError && ((TbActorError) t).isUnrecoverable(); + return t instanceof RecoveryAware recoveryAware && recoveryAware.isUnrecoverable(); } private void enqueue(TbActorMsg msg, boolean highPriority) { diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/TbScriptException.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/TbScriptException.java index 77888db255..347490b3fb 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/TbScriptException.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/TbScriptException.java @@ -16,13 +16,24 @@ package org.thingsboard.script.api; import lombok.Getter; +import org.thingsboard.common.util.RecoveryAware; +import java.io.Serial; import java.util.UUID; -public class TbScriptException extends RuntimeException { +public class TbScriptException extends RuntimeException implements RecoveryAware { + + @Serial private static final long serialVersionUID = -1958193538782818284L; - public static enum ErrorCode {COMPILATION, TIMEOUT, RUNTIME, OTHER} + public enum ErrorCode { + + COMPILATION, + TIMEOUT, + RUNTIME, + OTHER + + } @Getter private final UUID scriptId; @@ -37,4 +48,10 @@ public class TbScriptException extends RuntimeException { this.errorCode = errorCode; this.body = body; } + + @Override + public boolean isUnrecoverable() { + return errorCode == ErrorCode.COMPILATION; + } + } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java index 3507dd87e6..4aedab8081 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java @@ -20,6 +20,7 @@ import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import delight.nashornsandbox.NashornSandbox; import delight.nashornsandbox.NashornSandboxes; +import delight.nashornsandbox.exceptions.ScriptCPUAbuseException; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.Getter; @@ -153,8 +154,12 @@ public class NashornJsInvokeService extends AbstractJsInvokeService { } scriptInfoMap.put(scriptId, scriptInfo); return scriptId; - } catch (Exception e) { + } catch (ScriptException e) { throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, jsScript, e); + } catch (ScriptCPUAbuseException e) { + throw new TbScriptException(scriptId, TbScriptException.ErrorCode.TIMEOUT, jsScript, e); + } catch (Exception e) { + throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, jsScript, e); } }); } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java index 25a7ede547..e2195a6bbf 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java @@ -27,6 +27,7 @@ import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.mvel2.CompileException; import org.mvel2.ExecutionContext; import org.mvel2.MVEL; import org.mvel2.ParserContext; @@ -52,11 +53,11 @@ import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.util.Calendar; import java.util.Collections; -import java.util.Map; import java.util.Optional; import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executor; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -66,9 +67,9 @@ import java.util.concurrent.locks.ReentrantLock; @Service public class DefaultTbelInvokeService extends AbstractScriptInvokeService implements TbelInvokeService { - protected final Map scriptIdToHash = new ConcurrentHashMap<>(); - protected final Map scriptMap = new ConcurrentHashMap<>(); - protected Cache compiledScriptsCache; + private final ConcurrentMap scriptIdToHash = new ConcurrentHashMap<>(); + private final ConcurrentMap scriptMap = new ConcurrentHashMap<>(); + private Cache compiledScriptsCache; private SandboxedParserConfiguration parserConfig; private final Optional apiUsageStateClient; @@ -204,8 +205,10 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem lock.unlock(); } return scriptId; - } catch (Exception e) { + } catch (CompileException e) { throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, e); + } catch (Exception e) { + throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, scriptBody, e); } }); } @@ -246,7 +249,7 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem } } - private Serializable compileScript(String scriptBody) { + private static Serializable compileScript(String scriptBody) throws CompileException { return MVEL.compileExpression(scriptBody, new ParserContext()); } @@ -269,4 +272,5 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem protected StatsType getStatsType() { return StatsType.TBEL_INVOKE; } + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorError.java b/common/util/src/main/java/org/thingsboard/common/util/RecoveryAware.java similarity index 89% rename from common/message/src/main/java/org/thingsboard/server/common/msg/TbActorError.java rename to common/util/src/main/java/org/thingsboard/common/util/RecoveryAware.java index 8c322d8eb5..e1553bec36 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorError.java +++ b/common/util/src/main/java/org/thingsboard/common/util/RecoveryAware.java @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.msg; +package org.thingsboard.common.util; -public interface TbActorError { +public interface RecoveryAware { boolean isUnrecoverable(); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java index 874a37792d..7d5a7443d3 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java @@ -16,12 +16,9 @@ package org.thingsboard.rule.engine.api; import lombok.Getter; -import org.thingsboard.server.common.msg.TbActorError; +import org.thingsboard.common.util.RecoveryAware; -/** - * Created by ashvayka on 19.01.18. - */ -public class TbNodeException extends Exception implements TbActorError { +public class TbNodeException extends Exception implements RecoveryAware { @Getter private final boolean unrecoverable; From d4ec3f8b396294b6495e8e68fafcd5ef401f0087 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 7 Jul 2025 20:11:52 +0300 Subject: [PATCH 153/435] AI rule node: change structure of response format info in node config --- .../data/validation/ValidJsonSchema.java | 39 +++++++ .../common/util/JsonSchemaUtils.java | 12 +- .../dao/service/ConstraintValidator.java | 2 + .../dao/service/JsonSchemaValidator.java | 31 ++++++ .../thingsboard/rule/engine/ai/TbAiNode.java | 8 +- .../rule/engine/ai/TbAiNodeConfiguration.java | 21 +--- .../rule/engine/ai/TbResponseFormat.java | 103 ++++++++++++++++++ 7 files changed, 188 insertions(+), 28 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/validation/ValidJsonSchema.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/service/JsonSchemaValidator.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/validation/ValidJsonSchema.java b/common/data/src/main/java/org/thingsboard/server/common/data/validation/ValidJsonSchema.java new file mode 100644 index 0000000000..d37d7eb9e7 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/validation/ValidJsonSchema.java @@ -0,0 +1,39 @@ +/** + * 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.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Constraint(validatedBy = {}) +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidJsonSchema { + + String message() default "must conform to the Draft 2020-12 meta-schema"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/JsonSchemaUtils.java b/common/util/src/main/java/org/thingsboard/common/util/JsonSchemaUtils.java index db45994826..57ddd8a5ac 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JsonSchemaUtils.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JsonSchemaUtils.java @@ -15,7 +15,7 @@ */ package org.thingsboard.common.util; -import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.networknt.schema.JsonSchemaFactory; import com.networknt.schema.SchemaId; import com.networknt.schema.SchemaLocation; @@ -26,17 +26,15 @@ import java.util.Set; public final class JsonSchemaUtils { - private JsonSchemaUtils() { - throw new AssertionError("Can't instantiate utility class"); - } + private JsonSchemaUtils() {} /** - * Validates that the provided JsonNode is a valid JSON Schema (Draft 2020-12). + * Validates that the provided ObjectNode is a valid JSON Schema (Draft 2020-12). * - * @param schemaNode the JSON Schema document as a JsonNode + * @param schemaNode the JSON Schema document as an ObjectNode * @return true if the schema is well-formed, false otherwise */ - public static boolean isValidJsonSchema(JsonNode schemaNode) { + public static boolean isValidJsonSchema(ObjectNode schemaNode) { Set errors = JsonSchemaFactory .getInstance(SpecVersion.VersionFlag.V202012) .getSchema(SchemaLocation.of(SchemaId.V202012)) diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java index d220762f1b..0fd55bb494 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoNullChar; import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.RateLimit; +import org.thingsboard.server.common.data.validation.ValidJsonSchema; import org.thingsboard.server.dao.exception.DataValidationException; import java.util.Collection; @@ -107,6 +108,7 @@ public class ConstraintValidator { constraintMapping.constraintDefinition(Length.class).validatedBy(StringLengthValidator.class); constraintMapping.constraintDefinition(RateLimit.class).validatedBy(RateLimitValidator.class); constraintMapping.constraintDefinition(NoNullChar.class).validatedBy(NoNullCharValidator.class); + constraintMapping.constraintDefinition(ValidJsonSchema.class).validatedBy(JsonSchemaValidator.class); return constraintMapping; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/JsonSchemaValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/JsonSchemaValidator.java new file mode 100644 index 0000000000..eefefbb3d7 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/JsonSchemaValidator.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.thingsboard.common.util.JsonSchemaUtils; +import org.thingsboard.server.common.data.validation.ValidJsonSchema; + +public final class JsonSchemaValidator implements ConstraintValidator { + + @Override + public boolean isValid(ObjectNode schema, ConstraintValidatorContext context) { + return schema == null || JsonSchemaUtils.isValidJsonSchema(schema); + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index fe05712bea..62d92ef2ce 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -23,7 +23,6 @@ import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.request.ResponseFormat; -import dev.langchain4j.model.chat.request.ResponseFormatType; import dev.langchain4j.model.chat.response.ChatResponse; import org.checkerframework.checker.nullness.qual.NonNull; import org.thingsboard.common.util.JacksonUtil; @@ -81,11 +80,8 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { } // LC4j AnthropicChatModel rejects requests with non-null ResponseFormat even if ResponseFormatType is TEXT - if (config.getResponseFormatType() == ResponseFormatType.JSON) { - responseFormat = ResponseFormat.builder() - .type(config.getResponseFormatType()) - .jsonSchema(config.getJsonSchema() != null ? Langchain4jJsonSchemaAdapter.fromObjectNode(config.getJsonSchema()) : null) - .build(); + if (config.getResponseFormat().type() == TbResponseFormat.TbResponseFormatType.JSON) { + responseFormat = config.getResponseFormat().toLangChainResponseFormat(); } systemPrompt = config.getSystemPrompt(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index d9c815192d..ebfcf943f7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -15,21 +15,19 @@ */ package org.thingsboard.rule.engine.ai; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.databind.node.ObjectNode; -import dev.langchain4j.model.chat.request.ResponseFormatType; -import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import lombok.Data; -import org.thingsboard.common.util.JsonSchemaUtils; import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.validation.Length; +import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseFormat; + @Data public class TbAiNodeConfiguration implements NodeConfiguration { @@ -45,26 +43,19 @@ public class TbAiNodeConfiguration implements NodeConfiguration Date: Tue, 8 Jul 2025 11:40:18 +0300 Subject: [PATCH 154/435] Add admin settings entity type --- .../server/controller/AdminController.java | 51 ++--- .../service/mail/DefaultMailService.java | 183 ++++++----------- .../server/service/mail/TbMailSender.java | 22 +- .../service/security/permission/Resource.java | 2 +- .../service/sms/DefaultSmsSenderFactory.java | 16 +- .../dao/settings/AdminSettingsService.java | 5 +- .../server/common/data/EntityType.java | 6 +- .../common/data/id/AdminSettingsId.java | 18 +- .../server/common/data/id/EdgeId.java | 3 + .../server/common/data/id/EntityId.java | 4 - .../common/data/id/EntityIdFactory.java | 190 ++++++------------ .../server/common/data/id/JobId.java | 4 + .../server/common/data/id/TenantId.java | 2 + .../server/common/util/ProtoUtils.java | 12 +- common/proto/src/main/proto/queue.proto | 1 + .../HybridClientRegistrationRepository.java | 6 +- .../server/dao/settings/AdminSettingsDao.java | 14 +- .../settings/AdminSettingsServiceImpl.java | 22 +- .../sql/settings/AdminSettingsRepository.java | 3 - .../dao/sql/settings/JpaAdminSettingsDao.java | 34 ++-- .../server/dao/tenant/TenantServiceImpl.java | 8 +- .../rule/engine/util/TenantIdLoader.java | 1 + .../rule/engine/util/TenantIdLoaderTest.java | 8 +- 23 files changed, 252 insertions(+), 363 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index 6b3132a1d2..154e349fdb 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -37,14 +37,13 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +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.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; @@ -125,8 +124,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)", notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/settings/{key}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/settings/{key}") public AdminSettings getAdminSettings( @Parameter(description = "A string value of the key (e.g. 'general' or 'mail').") @PathVariable("key") String key) throws ThingsboardException { @@ -144,8 +142,7 @@ public class AdminController extends BaseController { "The Administration Settings Id will be present in the response. Specify the Administration Settings Id when you would like to update the Administration Settings. " + "Referencing non-existing Administration Settings Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/settings", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/settings") public AdminSettings saveAdminSettings( @Parameter(description = "A JSON value representing the Administration Settings.") @RequestBody AdminSettings adminSettings) throws ThingsboardException { @@ -165,8 +162,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Get the Security Settings object (getSecuritySettings)", notes = "Get the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/securitySettings", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/securitySettings") public SecuritySettings getSecuritySettings() throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); return checkNotNull(securitySettingsService.getSecuritySettings()); @@ -175,8 +171,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Update Security Settings (saveSecuritySettings)", notes = "Updates the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/securitySettings", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/securitySettings") public SecuritySettings saveSecuritySettings( @Parameter(description = "A JSON value representing the Security Settings.") @RequestBody SecuritySettings securitySettings) throws ThingsboardException { @@ -188,8 +183,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Get the JWT Settings object (getJwtSettings)", notes = "Get the JWT Settings object that contains JWT token policy, etc. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/jwtSettings", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/jwtSettings") public JwtSettings getJwtSettings() throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); return checkNotNull(jwtSettingsService.getJwtSettings()); @@ -198,8 +192,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Update JWT Settings (saveJwtSettings)", notes = "Updates the JWT Settings object that contains JWT token policy, etc. The tokenSigningKey field is a Base64 encoded string." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/jwtSettings", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/jwtSettings") public JwtPair saveJwtSettings( @Parameter(description = "A JSON value representing the JWT Settings.") @RequestBody JwtSettings jwtSettings) throws ThingsboardException { @@ -213,7 +206,7 @@ public class AdminController extends BaseController { notes = "Attempts to send test email to the System Administrator User using Mail Settings provided as a parameter. " + "You may change the 'To' email in the user profile of the System Administrator. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/settings/testMail", method = RequestMethod.POST) + @PostMapping(value = "/settings/testMail") public void sendTestMail( @Parameter(description = "A JSON value representing the Mail Settings.") @RequestBody AdminSettings adminSettings) throws ThingsboardException { @@ -251,7 +244,7 @@ public class AdminController extends BaseController { notes = "Attempts to send test sms to the System Administrator User using SMS Settings and phone number provided as a parameters of the request. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/settings/testSms", method = RequestMethod.POST) + @PostMapping(value = "/settings/testSms") public void sendTestSms( @Parameter(description = "A JSON value representing the Test SMS request.") @RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException { @@ -325,7 +318,7 @@ public class AdminController extends BaseController { notes = "Deletes the repository settings." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/repositorySettings", method = RequestMethod.DELETE) + @DeleteMapping(value = "/repositorySettings") @ResponseStatus(value = HttpStatus.OK) public DeferredResult deleteRepositorySettings() throws Exception { accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE); @@ -335,7 +328,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Check repository access (checkRepositoryAccess)", notes = "Attempts to check repository access. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/repositorySettings/checkAccess", method = RequestMethod.POST) + @PostMapping(value = "/repositorySettings/checkAccess") public DeferredResult checkRepositoryAccess( @Parameter(description = "A JSON value representing the Repository Settings.") @RequestBody RepositorySettings settings) throws Exception { @@ -376,7 +369,7 @@ public class AdminController extends BaseController { notes = "Deletes the auto commit settings." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/autoCommitSettings", method = RequestMethod.DELETE) + @DeleteMapping(value = "/autoCommitSettings") @ResponseStatus(value = HttpStatus.OK) public void deleteAutoCommitSettings() throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE); @@ -387,9 +380,8 @@ public class AdminController extends BaseController { notes = "Check notifications about new platform releases. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/updates", method = RequestMethod.GET) - @ResponseBody - public UpdateMessage checkUpdates() throws ThingsboardException { + @GetMapping(value = "/updates") + public UpdateMessage checkUpdates() { return updateService.checkUpdates(); } @@ -397,9 +389,8 @@ public class AdminController extends BaseController { notes = "Get main information about system. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/systemInfo", method = RequestMethod.GET) - @ResponseBody - public SystemInfo getSystemInfo() throws ThingsboardException { + @GetMapping(value = "/systemInfo") + public SystemInfo getSystemInfo() { return systemInfoService.getSystemInfo(); } @@ -407,8 +398,7 @@ public class AdminController extends BaseController { notes = "Get information about enabled/disabled features. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/featuresInfo", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/featuresInfo") public FeaturesInfo getFeaturesInfo() { return systemInfoService.getFeaturesInfo(); } @@ -417,8 +407,7 @@ public class AdminController extends BaseController { "double quotes. After successful authentication with OAuth2 provider and user consent for requested scope, it makes a redirect to this path so that the platform can do " + "further log in processing and generating access tokens. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @RequestMapping(value = "/mail/oauth2/loginProcessingUrl", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/mail/oauth2/loginProcessingUrl") public String getMailProcessingUrl() throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); return "\"/api/admin/mail/oauth2/code\""; @@ -427,7 +416,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Redirect user to mail provider login page. ", notes = "After user logged in and provided access" + "provider sends authorization code to specified redirect uri.)") @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/mail/oauth2/authorize", method = RequestMethod.GET, produces = "application/text") + @GetMapping(value = "/mail/oauth2/authorize", produces = "application/text") public String getAuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException { String state = StringUtils.generateSafeToken(); if (request.getParameter(PREV_URI_PATH_PARAMETER) != null) { @@ -452,7 +441,7 @@ public class AdminController extends BaseController { .build() + "\""; } - @RequestMapping(value = "/mail/oauth2/code", params = {"code", "state"}, method = RequestMethod.GET) + @GetMapping(value = "/mail/oauth2/code", params = {"code", "state"}) public void codeProcessingUrl( @RequestParam(value = "code") String code, @RequestParam(value = "state") String state, HttpServletRequest request, HttpServletResponse response) throws ThingsboardException, IOException { diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 99faf1a1d0..c1cfe791da 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -22,9 +22,9 @@ import freemarker.template.Template; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Lazy; @@ -64,55 +64,37 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -@Service @Slf4j +@Service +@RequiredArgsConstructor public class DefaultMailService implements MailService { - public static final String TARGET_EMAIL = "targetEmail"; - public static final String UTF_8 = "UTF-8"; + private static final String TARGET_EMAIL = "targetEmail"; + private static final String UTF_8 = "UTF-8"; + private static final long DEFAULT_TIMEOUT = 10_000; + + private final ScheduledExecutorService timeoutScheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("mail-service-watchdog"); private final MessageSource messages; private final Configuration freemarkerConfig; private final AdminSettingsService adminSettingsService; private final TbApiUsageReportClient apiUsageClient; - - private static final long DEFAULT_TIMEOUT = 10_000; - @Lazy - @Autowired - private TbApiUsageStateService apiUsageStateService; - - @Autowired - private MailSenderInternalExecutorService mailExecutorService; - - @Autowired - private PasswordResetExecutorService passwordResetExecutorService; - - @Autowired - private TbMailContextComponent ctx; - - @Autowired - private RateLimitService rateLimitService; + private final TbApiUsageStateService apiUsageStateService; + private final MailSenderInternalExecutorService mailExecutorService; + private final PasswordResetExecutorService passwordResetExecutorService; + private final TbMailContextComponent ctx; + private final RateLimitService rateLimitService; @Value("${mail.per_tenant_rate_limits:}") private String perTenantRateLimitConfig; - private final ScheduledExecutorService timeoutScheduler; - private TbMailSender mailSender; private String mailFrom; private long timeout; - public DefaultMailService(MessageSource messages, Configuration freemarkerConfig, AdminSettingsService adminSettingsService, TbApiUsageReportClient apiUsageClient) { - this.messages = messages; - this.freemarkerConfig = freemarkerConfig; - this.adminSettingsService = adminSettingsService; - this.apiUsageClient = apiUsageClient; - this.timeoutScheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("mail-service-watchdog"); - } - @PostConstruct private void init() { updateMailConfiguration(); @@ -120,9 +102,7 @@ public class DefaultMailService implements MailService { @PreDestroy public void destroy() { - if (timeoutScheduler != null) { - timeoutScheduler.shutdownNow(); - } + timeoutScheduler.shutdownNow(); } @Override @@ -311,22 +291,21 @@ public class DefaultMailService implements MailService { model.put("apiFeature", apiFeature.getLabel()); model.put(TARGET_EMAIL, email); - String message = null; - - switch (stateValue) { - case ENABLED: + String message = switch (stateValue) { + case ENABLED -> { model.put("apiLabel", toEnabledValueLabel(apiFeature)); - message = mergeTemplateIntoString("state.enabled.ftl", model); - break; - case WARNING: + yield mergeTemplateIntoString("state.enabled.ftl", model); + } + case WARNING -> { model.put("apiValueLabel", toDisabledValueLabel(apiFeature) + " " + toWarningValueLabel(recordState)); - message = mergeTemplateIntoString("state.warning.ftl", model); - break; - case DISABLED: + yield mergeTemplateIntoString("state.warning.ftl", model); + } + case DISABLED -> { model.put("apiLimitValueLabel", toDisabledValueLabel(apiFeature) + " " + toDisabledValueLabel(recordState)); - message = mergeTemplateIntoString("state.disabled.ftl", model); - break; - } + yield mergeTemplateIntoString("state.disabled.ftl", model); + } + }; + sendMail(mailSender, mailFrom, email, subject, message, timeout); } @@ -341,89 +320,55 @@ public class DefaultMailService implements MailService { } private String toEnabledValueLabel(ApiFeature apiFeature) { - switch (apiFeature) { - case DB: - return "save"; - case TRANSPORT: - return "receive"; - case JS: - return "invoke"; - case RE: - return "process"; - case EMAIL: - case SMS: - return "send"; - case ALARM: - return "create"; - default: - throw new RuntimeException("Not implemented!"); - } + return switch (apiFeature) { + case DB -> "save"; + case TRANSPORT -> "receive"; + case JS -> "invoke"; + case RE -> "process"; + case EMAIL, SMS -> "send"; + case ALARM -> "create"; + default -> throw new RuntimeException("Not implemented!"); + }; } private String toDisabledValueLabel(ApiFeature apiFeature) { - switch (apiFeature) { - case DB: - return "saved"; - case TRANSPORT: - return "received"; - case JS: - return "invoked"; - case RE: - return "processed"; - case EMAIL: - case SMS: - return "sent"; - case ALARM: - return "created"; - default: - throw new RuntimeException("Not implemented!"); - } + return switch (apiFeature) { + case DB -> "saved"; + case TRANSPORT -> "received"; + case JS -> "invoked"; + case RE -> "processed"; + case EMAIL, SMS -> "sent"; + case ALARM -> "created"; + default -> throw new RuntimeException("Not implemented!"); + }; } private String toWarningValueLabel(ApiUsageRecordState recordState) { String valueInM = recordState.getValueAsString(); String thresholdInM = recordState.getThresholdAsString(); - switch (recordState.getKey()) { - case STORAGE_DP_COUNT: - case TRANSPORT_DP_COUNT: - return valueInM + " out of " + thresholdInM + " allowed data points"; - case TRANSPORT_MSG_COUNT: - return valueInM + " out of " + thresholdInM + " allowed messages"; - case JS_EXEC_COUNT: - return valueInM + " out of " + thresholdInM + " allowed JavaScript functions"; - case TBEL_EXEC_COUNT: - return valueInM + " out of " + thresholdInM + " allowed Tbel functions"; - case RE_EXEC_COUNT: - return valueInM + " out of " + thresholdInM + " allowed Rule Engine messages"; - case EMAIL_EXEC_COUNT: - return valueInM + " out of " + thresholdInM + " allowed Email messages"; - case SMS_EXEC_COUNT: - return valueInM + " out of " + thresholdInM + " allowed SMS messages"; - default: - throw new RuntimeException("Not implemented!"); - } + return switch (recordState.getKey()) { + case STORAGE_DP_COUNT, TRANSPORT_DP_COUNT -> valueInM + " out of " + thresholdInM + " allowed data points"; + case TRANSPORT_MSG_COUNT -> valueInM + " out of " + thresholdInM + " allowed messages"; + case JS_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed JavaScript functions"; + case TBEL_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed Tbel functions"; + case RE_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed Rule Engine messages"; + case EMAIL_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed Email messages"; + case SMS_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed SMS messages"; + default -> throw new RuntimeException("Not implemented!"); + }; } private String toDisabledValueLabel(ApiUsageRecordState recordState) { - switch (recordState.getKey()) { - case STORAGE_DP_COUNT: - case TRANSPORT_DP_COUNT: - return recordState.getValueAsString() + " data points"; - case TRANSPORT_MSG_COUNT: - return recordState.getValueAsString() + " messages"; - case JS_EXEC_COUNT: - return "JavaScript functions " + recordState.getValueAsString() + " times"; - case TBEL_EXEC_COUNT: - return "TBEL functions " + recordState.getValueAsString() + " times"; - case RE_EXEC_COUNT: - return recordState.getValueAsString() + " Rule Engine messages"; - case EMAIL_EXEC_COUNT: - return recordState.getValueAsString() + " Email messages"; - case SMS_EXEC_COUNT: - return recordState.getValueAsString() + " SMS messages"; - default: - throw new RuntimeException("Not implemented!"); - } + return switch (recordState.getKey()) { + case STORAGE_DP_COUNT, TRANSPORT_DP_COUNT -> recordState.getValueAsString() + " data points"; + case TRANSPORT_MSG_COUNT -> recordState.getValueAsString() + " messages"; + case JS_EXEC_COUNT -> "JavaScript functions " + recordState.getValueAsString() + " times"; + case TBEL_EXEC_COUNT -> "TBEL functions " + recordState.getValueAsString() + " times"; + case RE_EXEC_COUNT -> recordState.getValueAsString() + " Rule Engine messages"; + case EMAIL_EXEC_COUNT -> recordState.getValueAsString() + " Email messages"; + case SMS_EXEC_COUNT -> recordState.getValueAsString() + " SMS messages"; + default -> throw new RuntimeException("Not implemented!"); + }; } private void sendMail(JavaMailSenderImpl mailSender, String mailFrom, String email, diff --git a/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java b/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java index 8d7356a507..914a53e8a6 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java @@ -25,6 +25,7 @@ import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.gson.GsonFactory; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.lang.Nullable; import org.springframework.mail.MailException; @@ -50,8 +51,10 @@ public class TbMailSender extends JavaMailSenderImpl { private final TbMailContextComponent ctx; private final Lock lock; + @Getter private final Boolean oauth2Enabled; private volatile String accessToken; + @Getter private volatile long tokenExpires; public TbMailSender(TbMailContextComponent ctx, JsonNode jsonConfig) { @@ -70,14 +73,6 @@ public class TbMailSender extends JavaMailSenderImpl { setJavaMailProperties(createJavaMailProperties(jsonConfig)); } - public Boolean getOauth2Enabled() { - return oauth2Enabled; - } - - public long getTokenExpires() { - return tokenExpires; - } - @Override protected void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) throws MailException { updateOauth2PasswordIfExpired(); @@ -98,8 +93,8 @@ public class TbMailSender extends JavaMailSenderImpl { super.testConnection(); } - public void updateOauth2PasswordIfExpired() { - if (getOauth2Enabled() && (System.currentTimeMillis() > getTokenExpires())){ + public void updateOauth2PasswordIfExpired() { + if (getOauth2Enabled() && (System.currentTimeMillis() > getTokenExpires())) { refreshAccessToken(); setPassword(accessToken); } @@ -168,8 +163,8 @@ public class TbMailSender extends JavaMailSenderImpl { .setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret)) .execute(); if (MailOauth2Provider.OFFICE_365.name().equals(providerId)) { - ((ObjectNode)jsonValue).put("refreshToken", tokenResponse.getRefreshToken()); - ((ObjectNode)jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli()); + ((ObjectNode) jsonValue).put("refreshToken", tokenResponse.getRefreshToken()); + ((ObjectNode) jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli()); ctx.getAdminSettingsService().saveAdminSettings(TenantId.SYS_TENANT_ID, settings); } accessToken = tokenResponse.getAccessToken(); @@ -190,4 +185,5 @@ public class TbMailSender extends JavaMailSenderImpl { throw new IncorrectParameterException(String.format("Invalid smtp port value: %s", strPort)); } } -} \ No newline at end of file + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index 2a92c040e3..22b75ea3f5 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -21,7 +21,7 @@ import java.util.Collections; import java.util.Set; public enum Resource { - ADMIN_SETTINGS(), + ADMIN_SETTINGS(EntityType.ADMIN_SETTINGS), ALARM(EntityType.ALARM), DEVICE(EntityType.DEVICE), ASSET(EntityType.ASSET), diff --git a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java index 5522047deb..475c9c2c75 100644 --- a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java @@ -31,16 +31,12 @@ public class DefaultSmsSenderFactory implements SmsSenderFactory { @Override public SmsSender createSmsSender(SmsProviderConfiguration config) { - switch (config.getType()) { - case AWS_SNS: - return new AwsSmsSender((AwsSnsSmsProviderConfiguration)config); - case TWILIO: - return new TwilioSmsSender((TwilioSmsProviderConfiguration)config); - case SMPP: - return new SmppSmsSender((SmppSmsProviderConfiguration) config); - default: - throw new RuntimeException("Unknown SMS provider type " + config.getType()); - } + return switch (config.getType()) { + case AWS_SNS -> new AwsSmsSender((AwsSnsSmsProviderConfiguration) config); + case TWILIO -> new TwilioSmsSender((TwilioSmsProviderConfiguration) config); + case SMPP -> new SmppSmsSender((SmppSmsProviderConfiguration) config); + default -> throw new RuntimeException("Unknown SMS provider type " + config.getType()); + }; } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java index b803700582..5223a2a9c9 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java @@ -18,8 +18,9 @@ package org.thingsboard.server.dao.settings; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.id.AdminSettingsId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.entity.EntityDaoService; -public interface AdminSettingsService { +public interface AdminSettingsService extends EntityDaoService { AdminSettings findAdminSettingsById(TenantId tenantId, AdminSettingsId adminSettingsId); @@ -31,6 +32,4 @@ public interface AdminSettingsService { boolean deleteAdminSettingsByTenantIdAndKey(TenantId tenantId, String key); - void deleteAdminSettingsByTenantId(TenantId tenantId); - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index af5fec1827..c2614ccec6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -22,9 +22,6 @@ import java.util.Arrays; import java.util.EnumSet; import java.util.List; -/** - * @author Andrew Shvayka - */ public enum EntityType { TENANT(1), CUSTOMER(2), @@ -65,7 +62,8 @@ public enum EntityType { MOBILE_APP_BUNDLE(38), CALCULATED_FIELD(39), CALCULATED_FIELD_LINK(40), - JOB(41); + JOB(41), + ADMIN_SETTINGS(42); @Getter private final int protoNumber; // Corresponds to EntityTypeProto diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java index 8f2104b5fd..05194b8df5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java @@ -17,14 +17,26 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; -public class AdminSettingsId extends UUIDBased { +public class AdminSettingsId extends UUIDBased implements EntityId { + + @Serial + private static final long serialVersionUID = -4208011957475806567L; @JsonCreator - public AdminSettingsId(@JsonProperty("id") UUID id){ + public AdminSettingsId(@JsonProperty("id") UUID id) { super(id); } - + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ADMIN_SETTINGS", allowableValues = "ADMIN_SETTINGS") + @Override + public EntityType getEntityType() { + return EntityType.ADMIN_SETTINGS; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java index 9a20be4eb9..4ccabd39b8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java @@ -23,10 +23,12 @@ import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; import org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; public class EdgeId extends UUIDBased implements EntityId { + @Serial private static final long serialVersionUID = 1L; @JsonIgnore @@ -51,4 +53,5 @@ public class EdgeId extends UUIDBased implements EntityId { public static EdgeId fromUUID(@JsonProperty("id") UUID id) { return edges.computeIfAbsent(id, EdgeId::new); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java index 24196e28b5..ddfcd85dc8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java @@ -24,10 +24,6 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serializable; import java.util.UUID; -/** - * @author Andrew Shvayka - */ - @JsonDeserialize(using = EntityIdDeserializer.class) @JsonSerialize(using = EntityIdSerializer.class) @Schema diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index d23e4c078d..fa4421e2c2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -20,9 +20,6 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import java.util.UUID; -/** - * Created by ashvayka on 25.04.17. - */ public class EntityIdFactory { public static EntityId getByTypeAndUuid(int type, String uuid) { @@ -50,131 +47,74 @@ public class EntityIdFactory { } public static EntityId getByTypeAndUuid(EntityType type, UUID uuid) { - switch (type) { - case TENANT: - return TenantId.fromUUID(uuid); - case CUSTOMER: - return new CustomerId(uuid); - case USER: - return new UserId(uuid); - case DASHBOARD: - return new DashboardId(uuid); - case DEVICE: - return new DeviceId(uuid); - case ASSET: - return new AssetId(uuid); - case ALARM: - return new AlarmId(uuid); - case RULE_CHAIN: - return new RuleChainId(uuid); - case RULE_NODE: - return new RuleNodeId(uuid); - case ENTITY_VIEW: - return new EntityViewId(uuid); - case WIDGETS_BUNDLE: - return new WidgetsBundleId(uuid); - case WIDGET_TYPE: - return new WidgetTypeId(uuid); - case DEVICE_PROFILE: - return new DeviceProfileId(uuid); - case ASSET_PROFILE: - return new AssetProfileId(uuid); - case TENANT_PROFILE: - return new TenantProfileId(uuid); - case API_USAGE_STATE: - return new ApiUsageStateId(uuid); - case TB_RESOURCE: - return new TbResourceId(uuid); - case OTA_PACKAGE: - return new OtaPackageId(uuid); - case EDGE: - return new EdgeId(uuid); - case RPC: - return new RpcId(uuid); - case QUEUE: - return new QueueId(uuid); - case NOTIFICATION_TARGET: - return new NotificationTargetId(uuid); - case NOTIFICATION_REQUEST: - return new NotificationRequestId(uuid); - case NOTIFICATION_RULE: - return new NotificationRuleId(uuid); - case NOTIFICATION_TEMPLATE: - return new NotificationTemplateId(uuid); - case NOTIFICATION: - return new NotificationId(uuid); - case QUEUE_STATS: - return new QueueStatsId(uuid); - case OAUTH2_CLIENT: - return new OAuth2ClientId(uuid); - case MOBILE_APP: - return new MobileAppId(uuid); - case DOMAIN: - return new DomainId(uuid); - case MOBILE_APP_BUNDLE: - return new MobileAppBundleId(uuid); - case CALCULATED_FIELD: - return new CalculatedFieldId(uuid); - case CALCULATED_FIELD_LINK: - return new CalculatedFieldLinkId(uuid); - case JOB: - return new JobId(uuid); - } - throw new IllegalArgumentException("EntityType " + type + " is not supported!"); + return switch (type) { + case TENANT -> TenantId.fromUUID(uuid); + case CUSTOMER -> new CustomerId(uuid); + case USER -> new UserId(uuid); + case DASHBOARD -> new DashboardId(uuid); + case DEVICE -> new DeviceId(uuid); + case ASSET -> new AssetId(uuid); + case ALARM -> new AlarmId(uuid); + case RULE_CHAIN -> new RuleChainId(uuid); + case RULE_NODE -> new RuleNodeId(uuid); + case ENTITY_VIEW -> new EntityViewId(uuid); + case WIDGETS_BUNDLE -> new WidgetsBundleId(uuid); + case WIDGET_TYPE -> new WidgetTypeId(uuid); + case DEVICE_PROFILE -> new DeviceProfileId(uuid); + case ASSET_PROFILE -> new AssetProfileId(uuid); + case TENANT_PROFILE -> new TenantProfileId(uuid); + case API_USAGE_STATE -> new ApiUsageStateId(uuid); + case TB_RESOURCE -> new TbResourceId(uuid); + case OTA_PACKAGE -> new OtaPackageId(uuid); + case EDGE -> EdgeId.fromUUID(uuid); + case RPC -> new RpcId(uuid); + case QUEUE -> new QueueId(uuid); + case NOTIFICATION_TARGET -> new NotificationTargetId(uuid); + case NOTIFICATION_REQUEST -> new NotificationRequestId(uuid); + case NOTIFICATION_RULE -> new NotificationRuleId(uuid); + case NOTIFICATION_TEMPLATE -> new NotificationTemplateId(uuid); + case NOTIFICATION -> new NotificationId(uuid); + case QUEUE_STATS -> new QueueStatsId(uuid); + case OAUTH2_CLIENT -> new OAuth2ClientId(uuid); + case MOBILE_APP -> new MobileAppId(uuid); + case DOMAIN -> new DomainId(uuid); + case MOBILE_APP_BUNDLE -> new MobileAppBundleId(uuid); + case CALCULATED_FIELD -> new CalculatedFieldId(uuid); + case CALCULATED_FIELD_LINK -> new CalculatedFieldLinkId(uuid); + case JOB -> new JobId(uuid); + case ADMIN_SETTINGS -> new AdminSettingsId(uuid); + default -> throw new IllegalArgumentException("EntityType " + type + " is not supported!"); + }; } public static EntityId getByEdgeEventTypeAndUuid(EdgeEventType edgeEventType, UUID uuid) { - switch (edgeEventType) { - case TENANT: - return TenantId.fromUUID(uuid); - case CUSTOMER: - return new CustomerId(uuid); - case USER: - return new UserId(uuid); - case DASHBOARD: - return new DashboardId(uuid); - case DEVICE: - return new DeviceId(uuid); - case ASSET: - return new AssetId(uuid); - case ALARM: - return new AlarmId(uuid); - case RULE_CHAIN: - return new RuleChainId(uuid); - case ENTITY_VIEW: - return new EntityViewId(uuid); - case WIDGETS_BUNDLE: - return new WidgetsBundleId(uuid); - case WIDGET_TYPE: - return new WidgetTypeId(uuid); - case DEVICE_PROFILE: - return new DeviceProfileId(uuid); - case ASSET_PROFILE: - return new AssetProfileId(uuid); - case TENANT_PROFILE: - return new TenantProfileId(uuid); - case OTA_PACKAGE: - return new OtaPackageId(uuid); - case EDGE: - return new EdgeId(uuid); - case QUEUE: - return new QueueId(uuid); - case TB_RESOURCE: - return new TbResourceId(uuid); - case NOTIFICATION_RULE: - return new NotificationRuleId(uuid); - case NOTIFICATION_TARGET: - return new NotificationTargetId(uuid); - case NOTIFICATION_TEMPLATE: - return new NotificationTemplateId(uuid); - case OAUTH2_CLIENT: - return new OAuth2ClientId(uuid); - case DOMAIN: - return new DomainId(uuid); - case CALCULATED_FIELD: - return new CalculatedFieldId(uuid); - } - throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); + return switch (edgeEventType) { + case TENANT -> TenantId.fromUUID(uuid); + case CUSTOMER -> new CustomerId(uuid); + case USER -> new UserId(uuid); + case DASHBOARD -> new DashboardId(uuid); + case DEVICE -> new DeviceId(uuid); + case ASSET -> new AssetId(uuid); + case ALARM -> new AlarmId(uuid); + case RULE_CHAIN -> new RuleChainId(uuid); + case ENTITY_VIEW -> new EntityViewId(uuid); + case WIDGETS_BUNDLE -> new WidgetsBundleId(uuid); + case WIDGET_TYPE -> new WidgetTypeId(uuid); + case DEVICE_PROFILE -> new DeviceProfileId(uuid); + case ASSET_PROFILE -> new AssetProfileId(uuid); + case TENANT_PROFILE -> new TenantProfileId(uuid); + case OTA_PACKAGE -> new OtaPackageId(uuid); + case EDGE -> EdgeId.fromUUID(uuid); + case QUEUE -> new QueueId(uuid); + case TB_RESOURCE -> new TbResourceId(uuid); + case NOTIFICATION_RULE -> new NotificationRuleId(uuid); + case NOTIFICATION_TARGET -> new NotificationTargetId(uuid); + case NOTIFICATION_TEMPLATE -> new NotificationTemplateId(uuid); + case OAUTH2_CLIENT -> new OAuth2ClientId(uuid); + case DOMAIN -> new DomainId(uuid); + case CALCULATED_FIELD -> new CalculatedFieldId(uuid); + default -> throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); + }; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java index 76678b8b31..372452825f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java @@ -20,10 +20,14 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; public class JobId extends UUIDBased implements EntityId { + @Serial + private static final long serialVersionUID = -2225072123132918395L; + @JsonCreator public JobId(@JsonProperty("id") UUID id) { super(id); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java index 57561fb7ee..7cad1698d7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java @@ -23,6 +23,7 @@ import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; import org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; public final class TenantId extends UUIDBased implements EntityId { @@ -33,6 +34,7 @@ public final class TenantId extends UUIDBased implements EntityId { @JsonIgnore public static final TenantId SYS_TENANT_ID = TenantId.fromUUID(EntityId.NULL_UUID); + @Serial private static final long serialVersionUID = 1L; @JsonCreator diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index 3cded5a491..6d010a7a0d 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -252,7 +252,7 @@ public class ProtoUtils { public static EdgeEvent fromProto(TransportProtos.EdgeEventMsgProto proto) { EdgeEvent edgeEvent = new EdgeEvent(); - TenantId tenantId = new TenantId(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())); + TenantId tenantId = TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())); edgeEvent.setTenantId(tenantId); edgeEvent.setType(EdgeEventType.valueOf(proto.getEntityType())); edgeEvent.setAction(EdgeEventActionType.valueOf(proto.getAction())); @@ -845,7 +845,7 @@ public class ProtoUtils { public static Device fromProto(TransportProtos.DeviceProto proto) { Device device = new Device(getEntityId(proto.getDeviceIdMSB(), proto.getDeviceIdLSB(), DeviceId::new)); device.setCreatedTime(proto.getCreatedTime()); - device.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::new)); + device.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::fromUUID)); device.setName(proto.getDeviceName()); device.setType(proto.getDeviceType()); device.setDeviceProfileId(getEntityId(proto.getDeviceProfileIdMSB(), proto.getDeviceProfileIdLSB(), DeviceProfileId::new)); @@ -937,7 +937,7 @@ public class ProtoUtils { public static DeviceProfile fromProto(TransportProtos.DeviceProfileProto proto) { DeviceProfile deviceProfile = new DeviceProfile(getEntityId(proto.getDeviceProfileIdMSB(), proto.getDeviceProfileIdLSB(), DeviceProfileId::new)); deviceProfile.setCreatedTime(proto.getCreatedTime()); - deviceProfile.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::new)); + deviceProfile.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::fromUUID)); deviceProfile.setName(proto.getName()); deviceProfile.setDefault(proto.getIsDefault()); deviceProfile.setType(DeviceProfileType.valueOf(proto.getType())); @@ -1028,7 +1028,7 @@ public class ProtoUtils { } public static Tenant fromProto(TransportProtos.TenantProto proto) { - Tenant tenant = new Tenant(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::new)); + Tenant tenant = new Tenant(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::fromUUID)); tenant.setCreatedTime(proto.getCreatedTime()); tenant.setTenantProfileId(getEntityId(proto.getTenantProfileIdMSB(), proto.getTenantProfileIdLSB(), TenantProfileId::new)); tenant.setTitle(proto.getTitle()); @@ -1142,7 +1142,7 @@ public class ProtoUtils { public static TbResource fromProto(TransportProtos.TbResourceProto proto) { TbResource resource = new TbResource(getEntityId(proto.getResourceIdMSB(), proto.getResourceIdLSB(), TbResourceId::new)); - resource.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::new)); + resource.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::fromUUID)); resource.setCreatedTime(proto.getCreatedTime()); resource.setTitle(proto.getTitle()); resource.setResourceType(ResourceType.valueOf(proto.getResourceType())); @@ -1198,7 +1198,7 @@ public class ProtoUtils { public static ApiUsageState fromProto(TransportProtos.ApiUsageStateProto proto) { ApiUsageState apiUsageState = new ApiUsageState(getEntityId(proto.getApiUsageStateIdMSB(), proto.getApiUsageStateIdLSB(), ApiUsageStateId::new)); - apiUsageState.setTenantId(getEntityId(proto.getTenantProfileIdMSB(), proto.getTenantProfileIdLSB(), TenantId::new)); + apiUsageState.setTenantId(getEntityId(proto.getTenantProfileIdMSB(), proto.getTenantProfileIdLSB(), TenantId::fromUUID)); apiUsageState.setCreatedTime(proto.getCreatedTime()); apiUsageState.setEntityId(EntityIdFactory.getByTypeAndUuid(fromProto(proto.getEntityType()), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB()))); apiUsageState.setTransportState(ApiUsageStateValue.valueOf(proto.getTransportState())); diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 2667838b60..2709a2bdc5 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -64,6 +64,7 @@ enum EntityTypeProto { CALCULATED_FIELD = 39; CALCULATED_FIELD_LINK = 40; JOB = 41; + ADMIN_SETTINGS = 42; } enum ApiUsageRecordKeyProto { diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java index bc84a58cd9..84dc22b36b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java @@ -37,8 +37,10 @@ public class HybridClientRegistrationRepository implements ClientRegistrationRep @Override public ClientRegistration findByRegistrationId(String registrationId) { OAuth2Client oAuth2Client = oAuth2ClientService.findOAuth2ClientById(TenantId.SYS_TENANT_ID, new OAuth2ClientId(UUID.fromString(registrationId))); - return oAuth2Client == null ? - null : toSpringClientRegistration(oAuth2Client); + if (oAuth2Client == null) { + return null; + } + return toSpringClientRegistration(oAuth2Client); } private ClientRegistration toSpringClientRegistration(OAuth2Client oAuth2Client){ diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsDao.java index b330ce8ee3..8c184c8c35 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsDao.java @@ -23,20 +23,8 @@ import java.util.UUID; public interface AdminSettingsDao extends Dao { - /** - * Save or update admin settings object - * - * @param adminSettings the admin settings object - * @return saved admin settings object - */ AdminSettings save(TenantId tenantId, AdminSettings adminSettings); - - /** - * Find admin settings by key. - * - * @param key the key - * @return the admin settings object - */ + AdminSettings findByTenantIdAndKey(UUID tenantId, String key); boolean removeByTenantIdAndKey(UUID tenantId, String key); diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java index b1b3b25a19..604a37f4bb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java @@ -21,11 +21,16 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.AdminSettingsId; +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.dao.service.DataValidator; import org.thingsboard.server.dao.service.Validator; +import java.util.Optional; + @Service @Slf4j public class AdminSettingsServiceImpl implements AdminSettingsService { @@ -87,10 +92,25 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { } @Override - public void deleteAdminSettingsByTenantId(TenantId tenantId) { + public void deleteByTenantId(TenantId tenantId) { adminSettingsDao.removeByTenantId(tenantId.getId()); } + @Override + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + adminSettingsDao.removeById(tenantId, id.getId()); + } + + @Override + public Optional> findEntity(TenantId tenantId, EntityId entityId) { + return Optional.ofNullable(adminSettingsDao.findById(tenantId, entityId.getId())); + } + + @Override + public EntityType getEntityType() { + return EntityType.ADMIN_SETTINGS; + } + private void dropTokenIfProviderInfoChanged(JsonNode newJsonValue, JsonNode oldJsonValue) { if (newJsonValue.has("enableOauth2") && newJsonValue.get("enableOauth2").asBoolean()) { if (!newJsonValue.get("providerId").equals(oldJsonValue.get("providerId")) || diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java index c27602421d..889de7cb74 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java @@ -22,9 +22,6 @@ import org.thingsboard.server.dao.model.sql.AdminSettingsEntity; import java.util.UUID; -/** - * Created by Valerii Sosliuk on 5/6/2017. - */ public interface AdminSettingsRepository extends JpaRepository { AdminSettingsEntity findByTenantIdAndKey(UUID tenantId, String key); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java index 68ce5e9d22..3dafa79c3f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java @@ -15,12 +15,12 @@ */ package org.thingsboard.server.dao.sql.settings; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -35,21 +35,10 @@ import java.util.UUID; @Component @SqlDao -@Slf4j +@RequiredArgsConstructor public class JpaAdminSettingsDao extends JpaAbstractDao implements AdminSettingsDao, TenantEntityDao { - @Autowired - private AdminSettingsRepository adminSettingsRepository; - - @Override - protected Class getEntityClass() { - return AdminSettingsEntity.class; - } - - @Override - protected JpaRepository getRepository() { - return adminSettingsRepository; - } + private final AdminSettingsRepository adminSettingsRepository; @Override public AdminSettings findByTenantIdAndKey(UUID tenantId, String key) { @@ -77,4 +66,19 @@ public class JpaAdminSettingsDao extends JpaAbstractDao getEntityClass() { + return AdminSettingsEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return adminSettingsRepository; + } + + @Override + public EntityType getEntityType() { + return EntityType.ADMIN_SETTINGS; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index b9d09e55ba..45cb832fef 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -43,7 +43,6 @@ import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.TenantDataValidator; -import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.trendz.TrendzSettingsService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.dao.user.UserService; @@ -76,8 +75,6 @@ public class TenantServiceImpl extends AbstractCachedEntityService tenantDao.existsById(tenantId, tenantId.getId()), false); } - private PaginatedRemover tenantsRemover = new PaginatedRemover<>() { + private final PaginatedRemover tenantsRemover = new PaginatedRemover<>() { @Override protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java index 93fad4c0e7..f4a80937d7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -146,6 +146,7 @@ public class TenantIdLoader { tenantEntity = ctx.getNotificationRequestService().findNotificationRequestById(ctxTenantId, new NotificationRequestId(id)); break; case NOTIFICATION: + case ADMIN_SETTINGS: return ctxTenantId; case NOTIFICATION_RULE: tenantEntity = ctx.getNotificationRuleService().findNotificationRuleById(ctxTenantId, new NotificationRuleId(id)); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index 4cbc091bdc..60d2bd500a 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -29,6 +29,7 @@ import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; import org.thingsboard.rule.engine.api.RuleEngineRpcService; import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; @@ -167,7 +168,6 @@ public class TenantIdLoaderTest { private TenantId tenantId; private TenantProfileId tenantProfileId; - private NotificationId notificationId; private AbstractListeningExecutor dbExecutor; @BeforeEach @@ -179,9 +179,8 @@ public class TenantIdLoaderTest { } }; dbExecutor.init(); - this.tenantId = new TenantId(UUID.randomUUID()); + this.tenantId = TenantId.fromUUID(UUID.randomUUID()); this.tenantProfileId = new TenantProfileId(UUID.randomUUID()); - this.notificationId = new NotificationId(UUID.randomUUID()); when(ctx.getTenantId()).thenReturn(tenantId); @@ -199,6 +198,7 @@ public class TenantIdLoaderTest { switch (entityType) { case TENANT: case NOTIFICATION: + case ADMIN_SETTINGS: break; case CUSTOMER: Customer customer = new Customer(); @@ -465,7 +465,7 @@ public class TenantIdLoaderTest { @Test public void test_findEntityIdAsync_other_tenant() { - checkTenant(new TenantId(UUID.randomUUID()), false); + checkTenant(TenantId.fromUUID(UUID.randomUUID()), false); } } From 6ecb3a49f16db626b391432408f3833f26b16117 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 8 Jul 2025 12:53:27 +0300 Subject: [PATCH 155/435] UI: AI models --- .../thingsboard/rule/engine/ai/TbAiNode.java | 1 + ui-ngx/src/app/core/http/ai-model.service.ts | 54 ++++ ui-ngx/src/app/core/http/entity.service.ts | 9 + ui-ngx/src/app/core/http/public-api.ts | 1 + ui-ngx/src/app/core/services/menu.models.ts | 14 +- ui-ngx/src/app/modules/common/modules-map.ts | 6 + .../common/time-unit-input.component.html | 91 +++++-- .../common/time-unit-input.component.ts | 20 ++ .../external/ai-config.component.html | 115 ++++++++ .../rule-node/external/ai-config.component.ts | 115 ++++++++ .../external-rule-node-config.module.ts | 7 +- .../app/modules/home/models/services.map.ts | 4 +- .../pages/ai-model/ai-model-routing.module.ts | 49 ++++ .../ai-model/ai-model-table-config.resolve.ts | 124 +++++++++ .../ai-model-table-header.component.html | 21 ++ .../ai-model-table-header.component.scss | 18 ++ .../ai-model-table-header.component.ts | 33 +++ .../home/pages/ai-model/ai-model.module.ts | 33 +++ .../modules/home/pages/home-pages.module.ts | 4 +- .../ai-model/ai-model-dialog.component.html | 248 ++++++++++++++++++ .../ai-model/ai-model-dialog.component.scss | 23 ++ .../ai-model/ai-model-dialog.component.ts | 168 ++++++++++++ .../check-connectivity-dialog.component.html | 58 ++++ .../check-connectivity-dialog.component.scss | 71 +++++ .../check-connectivity-dialog.component.ts | 87 ++++++ .../models-list-autocomplete.component.html | 41 +++ .../models-list-autocomplete.component.ts | 161 ++++++++++++ .../entity/entity-autocomplete.component.ts | 17 +- .../src/app/shared/models/ai-model.models.ts | 159 +++++++++++ ui-ngx/src/app/shared/models/constants.ts | 1 + .../app/shared/models/entity-type.models.ts | 15 +- .../src/app/shared/models/id/ai-model-id.ts | 27 ++ ui-ngx/src/app/shared/models/id/public-api.ts | 1 + ui-ngx/src/app/shared/models/public-api.ts | 3 +- ui-ngx/src/app/shared/shared.module.ts | 9 + .../assets/locale/locale.constant-en_US.json | 97 ++++++- ui-ngx/src/form.scss | 30 +++ 37 files changed, 1892 insertions(+), 43 deletions(-) create mode 100644 ui-ngx/src/app/core/http/ai-model.service.ts create mode 100644 ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html create mode 100644 ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/ai-model/ai-model-routing.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts create mode 100644 ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/ai-model/ai-model.module.ts create mode 100644 ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html create mode 100644 ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.scss create mode 100644 ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts create mode 100644 ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.html create mode 100644 ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.scss create mode 100644 ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.ts create mode 100644 ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.html create mode 100644 ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts create mode 100644 ui-ngx/src/app/shared/models/ai-model.models.ts create mode 100644 ui-ngx/src/app/shared/models/id/ai-model-id.ts diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 62d92ef2ce..8c9017829a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -57,6 +57,7 @@ import static org.thingsboard.server.dao.service.ConstraintValidator.validateFie nodeDescription = "Interact with AI", nodeDetails = "This node makes requests to AI based on a prompt and a input message and returns a response in a form of output message", configClazz = TbAiNodeConfiguration.class, + configDirective = "tbExternalNodeAiConfig", ruleChainTypes = RuleChainType.CORE ) public final class TbAiNode extends TbAbstractExternalNode implements TbNode { diff --git a/ui-ngx/src/app/core/http/ai-model.service.ts b/ui-ngx/src/app/core/http/ai-model.service.ts new file mode 100644 index 0000000000..b2f63ba583 --- /dev/null +++ b/ui-ngx/src/app/core/http/ai-model.service.ts @@ -0,0 +1,54 @@ +/// +/// 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. +/// + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { Observable } from 'rxjs'; +import { AiModel, AiModelWithUserMsg, CheckConnectivityResult } from '@shared/models/ai-model.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; + +@Injectable({ + providedIn: 'root' +}) +export class AiModelService { + + constructor( + private http: HttpClient + ) {} + + public saveAiModel(aiModel: AiModel, config?: RequestConfig): Observable { + return this.http.post('/api/ai/model/settings', aiModel, defaultHttpOptionsFromConfig(config)); + } + + public getAiModels(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/ai/model/settings${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public getAiModelById(aiModelId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/ai/model/settings/${aiModelId}`, defaultHttpOptionsFromConfig(config)); + } + + public deleteAiModel(aiModelId: string, config?: RequestConfig) { + return this.http.delete(`/api/ai/model/settings/${aiModelId}`, defaultHttpOptionsFromConfig(config)); + } + + public checkConnectivity(aiModelWithUserMsg: AiModelWithUserMsg, config?: RequestConfig): Observable { + return this.http.post('/api/ai/model/chat', aiModelWithUserMsg, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 01fed28bf3..17a755ad30 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -99,6 +99,7 @@ import { ResourceService } from '@core/http/resource.service'; import { OAuth2Service } from '@core/http/oauth2.service'; import { MobileAppService } from '@core/http/mobile-app.service'; import { PlatformType } from '@shared/models/oauth2.models'; +import { AiModelService } from '@core/http/ai-model.service'; @Injectable({ providedIn: 'root' @@ -131,6 +132,7 @@ export class EntityService { private resourceService: ResourceService, private oauth2Service: OAuth2Service, private mobileAppService: MobileAppService, + private aiModelService: AiModelService, ) { } private getEntityObservable(entityType: EntityType, entityId: string, @@ -183,6 +185,9 @@ export class EntityService { case EntityType.MOBILE_APP_BUNDLE: observable = this.mobileAppService.getMobileAppBundleInfoById(entityId, config); break; + case EntityType.AI_MODEL_SETTINGS: + observable = this.aiModelService.getAiModelById(entityId, config); + break; } return observable; } @@ -485,6 +490,10 @@ export class EntityService { pageLink.sortOrder.property = 'title'; entitiesObservable = this.mobileAppService.getTenantMobileAppBundleInfos(pageLink, config); break; + case EntityType.AI_MODEL_SETTINGS: + pageLink.sortOrder.property = 'name'; + entitiesObservable = this.aiModelService.getAiModels(pageLink, config); + break; } return entitiesObservable; } diff --git a/ui-ngx/src/app/core/http/public-api.ts b/ui-ngx/src/app/core/http/public-api.ts index c28d80d173..63cc393ce6 100644 --- a/ui-ngx/src/app/core/http/public-api.ts +++ b/ui-ngx/src/app/core/http/public-api.ts @@ -48,3 +48,4 @@ export * from './user-settings.service'; export * from './widget.service'; export * from './usage-info.service'; export * from './trendz-settings.service' +export * from './ai-model.service' diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index 607c5c6dff..dc568c148e 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -105,7 +105,8 @@ export enum MenuId { otaUpdates = 'otaUpdates', version_control = 'version_control', api_usage = 'api_usage', - trendz_settings = 'trendz_settings' + trendz_settings = 'trendz_settings', + ai_models = 'ai_models' } declare type MenuFilter = (authState: AuthState) => boolean; @@ -286,6 +287,16 @@ export const menuSectionMap = new Map([ icon: 'mdi:message-cog' } ], + [ + MenuId.ai_models, + { + id: MenuId.ai_models, + name: 'ai-models.ai-models', + type: 'link', + path: '/ai-models', + icon: 'auto_awesome' + } + ], [ MenuId.mobile_center, { @@ -841,6 +852,7 @@ const defaultUserMenuMap = new Map([ {id: MenuId.notification_rules} ] }, + {id: MenuId.ai_models}, { id: MenuId.mobile_center, pages: [ diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 70432e8cdf..997af07462 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -336,6 +336,9 @@ import * as DatapointsLimitComponent from '@shared/components/time/datapoints-li import * as AggregationTypeSelectComponent from '@shared/components/time/aggregation/aggregation-type-select.component'; import * as AggregationOptionsConfigComponent from '@shared/components/time/aggregation/aggregation-options-config-panel.component'; import * as IntervalOptionsConfigPanelComponent from '@shared/components/time/interval-options-config-panel.component'; +import * as AIModelDialogComponent from '@shared/components/ai-model/ai-model-dialog.component'; +import * as CheckConnectivityDialogComponent from '@shared/components/ai-model/check-connectivity-dialog.component'; +import * as ModelsListAutocompleteComponent from '@shared/components/ai-model/models-list-autocomplete.component'; import { IModulesMap } from '@modules/common/modules-map.models'; import { Observable, of } from 'rxjs'; @@ -532,6 +535,9 @@ class ModulesMap implements IModulesMap { '@shared/components/image/gallery-image-input.component': GalleryImageInputComponent, '@shared/components/image/multiple-gallery-image-input.component': MultipleGalleryImageInputComponent, '@shared/components/popover.service': TbPopoverService, + '@shared/components/ai-model/ai-model-dialog.component': AIModelDialogComponent, + '@shared/components/ai-model/check-connectivity-dialog.component': CheckConnectivityDialogComponent, + '@shared/components/ai-model/models-list-autocomplete.component': ModelsListAutocompleteComponent, '@home/components/alarm/alarm-filter-config.component': AlarmFilterConfigComponent, diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.html index 37b0a2983c..3c1d85ed73 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.html @@ -16,29 +16,72 @@ -->
- - {{ labelText }} - -
- + @if (outlined) { +
+
{{ labelText }}
+
+ + + + warning + + + warning + + + warning + + + + + @for (timeUnit of timeUnits; track timeUnit) { + {{ timeUnitTranslations.get(timeUnit) | translate }} + } + + +
- - - {{ requiredText }} - - - {{ minErrorText }} - - - {{ maxErrorText }} - - - - rule-node-config.units - - @for (timeUnit of timeUnits; track timeUnit) { - {{ timeUnitTranslations.get(timeUnit) | translate }} - } - - + } @else { + + {{ labelText }} + +
+ +
+ + + {{ requiredText }} + + + {{ minErrorText }} + + + {{ maxErrorText }} + +
+ + rule-node-config.units + + @for (timeUnit of timeUnits; track timeUnit) { + {{ timeUnitTranslations.get(timeUnit) | translate }} + } + + + }
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts index b0d0a97641..2cd8e7d879 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts @@ -55,6 +55,9 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, @Input() labelText: string; + @Input() + labelIconHintTooltipText: string; + @Input() @coerceBoolean() required: boolean; @@ -79,6 +82,14 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, @Input() subscriptSizing: SubscriptSizing = 'fixed'; + @Input() + @coerceBoolean() + filterTimeUnitsByMaxTime = false; + + @Input() + @coerceBoolean() + outlined = false; + timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[]; timeUnitTranslations = timeUnitTranslations; @@ -104,6 +115,15 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, } ngOnInit() { + if (this.filterTimeUnitsByMaxTime && this.maxTime) { + if (this.maxTime < 60) { + this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.MINUTES && item !== TimeUnit.HOURS && item !== TimeUnit.DAYS); + } else if (this.maxTime < 3600) { + this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.HOURS && item !== TimeUnit.DAYS); + } else if (this.maxTime < 86400) { + this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.DAYS); + } + } if(this.required || this.maxTime) { const timeControl = this.timeInputForm.get('time'); const validators = [Validators.pattern(/^\d*$/)]; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html new file mode 100644 index 0000000000..f085913d5c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html @@ -0,0 +1,115 @@ + +
+
+
+ {{ 'rule-node-config.ai.ai-model' | translate }} +
+
+ + +
+
+ +
+ + + + {{'rule-node-config.ai.prompt-settings' | translate}} + + +
+ + rule-node-config.ai.system-prompt + + + {{ 'rule-node-config.ai.system-prompt-max-length' | translate }} + + + {{ 'rule-node-config.ai.system-prompt-blank' | translate }} + + + + rule-node-config.ai.user-prompt + + + {{ 'rule-node-config.ai.user-prompt-required' | translate }} + + + {{ 'rule-node-config.ai.user-prompt-max-length' | translate }} + + + {{ 'rule-node-config.ai.user-prompt-blank' | translate }} + + +
+
+
+ +
+
+
+ {{ 'rule-node-config.ai.response-format' | translate }} +
+ + {{ 'rule-node-config.ai.response-text' | translate }} + {{ 'rule-node-config.ai.response-json' | translate }} + {{ 'rule-node-config.ai.response-json-schema' | translate }} + +
+ @if (aiConfigForm.get('responseFormat.type').value === responseFormat.JSON_SCHEMA) { + + + } +
+ +
+ + + rule-node-config.ai.advanced-settings + +
+ + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts new file mode 100644 index 0000000000..283abb636c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts @@ -0,0 +1,115 @@ +/// +/// 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. +/// + +import { Component } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { MatDialog } from '@angular/material/dialog'; +import { AIModelDialogComponent, AIModelDialogData } from '@shared/components/ai-model/ai-model-dialog.component'; +import { AiModel, AiProvider } from '@shared/models/ai-model.models'; +import { deepTrim } from '@core/utils'; + +enum ResponseFormat { + TEXT = 'TEXT', + JSON = 'JSON', + JSON_SCHEMA = 'JSON_SCHEMA' +} + +@Component({ + selector: 'tb-external-node-ai-config', + templateUrl: './ai-config.component.html', + styleUrls: [] +}) +export class AiConfigComponent extends RuleNodeConfigurationComponent { + + aiConfigForm: UntypedFormGroup; + + entityType = EntityType; + + responseFormat = ResponseFormat; + + constructor(private fb: UntypedFormBuilder, + private dialog: MatDialog) { + super(); + } + + protected configForm(): UntypedFormGroup { + return this.aiConfigForm; + } + + protected onConfigurationSet(configuration: RuleNodeConfiguration) { + this.aiConfigForm = this.fb.group({ + modelSettingsId: [configuration ? configuration.modelSettingsId : null, [Validators.required]], + systemPrompt: [configuration ? configuration.systemPrompt : '', [Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], + userPrompt: [configuration ? configuration.userPrompt : '', [Validators.required, Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], + responseFormat: this.fb.group({ + type: [configuration ? configuration.responseFormat.type : ResponseFormat.JSON, []], + schema: [configuration ? configuration.responseFormat.schema : null, []], + }), + timeoutSeconds: [configuration ? configuration.timeoutSeconds : 60, []] + }); + } + + protected validatorTriggers(): string[] { + return ['responseFormat.type']; + } + + protected updateValidators(emitEvent: boolean) { + const responseFormatType = this.aiConfigForm.get('responseFormat.type').value; + if (responseFormatType === ResponseFormat.JSON_SCHEMA) { + this.aiConfigForm.get('responseFormat.schema').setValidators([Validators.required]); + this.aiConfigForm.get('responseFormat.schema').enable(); + } else { + this.aiConfigForm.get('responseFormat.schema').setValidators([]); + this.aiConfigForm.get('responseFormat.schema').disable(); + } + this.aiConfigForm.get('responseFormat.schema').updateValueAndValidity({emitEvent}); + } + + protected prepareOutputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration { + return deepTrim(configuration); + } + + onEntityChange($event: AiModel) { + if ($event) { + if ($event.configuration.provider === AiProvider.AMAZON_BEDROCK || + $event.configuration.provider === AiProvider.ANTHROPIC || + $event.configuration.provider === AiProvider.GITHUB_MODELS) { + this.aiConfigForm.get('responseFormat.type').patchValue(ResponseFormat.TEXT, {emitEvent: false}); + this.aiConfigForm.get('responseFormat.type').disable({emitEvent: false}); + } + } else { + this.aiConfigForm.get('responseFormat.type').enable({emitEvent: false}); + } + } + + createModelAi(formControl: string,) { + this.dialog.open(AIModelDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd: true + } + }).afterClosed() + .subscribe((model) => { + if (model) { + this.aiConfigForm.get(formControl).patchValue(model.id); + this.aiConfigForm.get(formControl).markAsDirty(); + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/external-rule-node-config.module.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/external-rule-node-config.module.ts index d2ed9c4f26..955c555989 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/external-rule-node-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/external-rule-node-config.module.ts @@ -32,6 +32,7 @@ import { HomeComponentsModule } from '@home/components/public-api'; import { CommonRuleNodeConfigModule } from '../common/common-rule-node-config.module'; import { SlackConfigComponent } from './slack-config.component'; import { LambdaConfigComponent } from './lambda-config.component'; +import { AiConfigComponent } from '@home/components/rule-node/external/ai-config.component'; @NgModule({ declarations: [ @@ -47,7 +48,8 @@ import { LambdaConfigComponent } from './lambda-config.component'; SendEmailConfigComponent, AzureIotHubConfigComponent, SendSmsConfigComponent, - SlackConfigComponent + SlackConfigComponent, + AiConfigComponent ], imports: [ CommonModule, @@ -68,7 +70,8 @@ import { LambdaConfigComponent } from './lambda-config.component'; SendEmailConfigComponent, AzureIotHubConfigComponent, SendSmsConfigComponent, - SlackConfigComponent + SlackConfigComponent, + AiConfigComponent ] }) export class ExternalRuleNodeConfigModule { diff --git a/ui-ngx/src/app/modules/home/models/services.map.ts b/ui-ngx/src/app/modules/home/models/services.map.ts index e4217b726b..ebccba900c 100644 --- a/ui-ngx/src/app/modules/home/models/services.map.ts +++ b/ui-ngx/src/app/modules/home/models/services.map.ts @@ -54,6 +54,7 @@ import { EventService } from '@core/http/event.service'; import { UnitService } from '@core/services/unit.service'; import { AuditLogService } from '@core/http/audit-log.service'; import { TrendzSettingsService } from '@core/http/trendz-settings.service'; +import { AiModelService } from '@core/http/ai-model.service'; export const ServicesMap = new Map>( [ @@ -95,6 +96,7 @@ export const ServicesMap = new Map>( ['eventService', EventService], ['unitService', UnitService], ['auditLogService', AuditLogService], - ['trendzSettingsService', TrendzSettingsService] + ['trendzSettingsService', TrendzSettingsService], + ['aiModelService', AiModelService] ] ); diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-routing.module.ts b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-routing.module.ts new file mode 100644 index 0000000000..82fff06270 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-routing.module.ts @@ -0,0 +1,49 @@ +/// +/// 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. +/// + + +import { RouterModule, Routes } from '@angular/router'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { MenuId } from '@core/services/menu.models'; +import { AiModelsTableConfigResolver } from '@home/pages/ai-model/ai-model-table-config.resolve'; +import { NgModule } from '@angular/core'; + +export const aiModelRoutes: Routes = [ + { + path: 'ai-models', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'ai-models.ai-models', + breadcrumb: { + menuId: MenuId.ai_models + } + }, + resolve: { + entitiesTableConfig: AiModelsTableConfigResolver + } + } +]; + +@NgModule({ + providers: [ + AiModelsTableConfigResolver + ], + imports: [RouterModule.forChild(aiModelRoutes)], + exports: [RouterModule], +}) +export class AiModelRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts new file mode 100644 index 0000000000..9f135d002c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts @@ -0,0 +1,124 @@ +/// +/// 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. +/// + +import { Injectable } from '@angular/core'; +import { + CellActionDescriptor, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { ActivatedRouteSnapshot } from '@angular/router'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { Direction } from '@shared/models/page/sort-order'; +import { DatePipe } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Observable } from 'rxjs'; +import { AiModel, AiProviderTranslations } from '@shared/models/ai-model.models'; +import { AiModelService } from '@core/http/ai-model.service'; +import { AiModelTableHeaderComponent } from '@home/pages/ai-model/ai-model-table-header.component'; +import { AIModelDialogComponent, AIModelDialogData } from '@shared/components/ai-model/ai-model-dialog.component'; +import { map } from 'rxjs/operators'; + +@Injectable() +export class AiModelsTableConfigResolver { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor( + private datePipe: DatePipe, + private aiModelService: AiModelService, + private translate : TranslateService, + private dialog: MatDialog + ) { + this.config.selectionEnabled = true; + this.config.entityType = EntityType.AI_MODEL_SETTINGS; + this.config.addAsTextButton = true; + this.config.detailsPanelEnabled = false; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.AI_MODEL_SETTINGS); + this.config.entityResources = entityTypeResources.get(EntityType.AI_MODEL_SETTINGS); + + this.config.headerComponent = AiModelTableHeaderComponent; + this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'}; + this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + + this.config.addEntity = () => this.addModel(null, true); + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '170px'), + new EntityTableColumn('name', 'ai-models.name', '33%'), + new EntityTableColumn('provider', 'ai-models.ai-provider', '33%', + entity => this.translate.instant(AiProviderTranslations.get(entity.configuration.provider)) + ), + new EntityTableColumn('aiModel', 'ai-models.ai-model', '33%', + entity => entity.configuration.modelConfig.modelId, () => ({}), false + ) + ) + + this.config.deleteEntityTitle = model => this.translate.instant('ai-models.delete-model-title', {modelName: model.name}); + this.config.deleteEntityContent = () => this.translate.instant('ai-models.delete-model-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('ai-models.delete-models-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('ai-models.delete-models-text'); + + this.config.deleteEntity = id => this.aiModelService.deleteAiModel(id.id); + + this.config.entitiesFetchFunction = pageLink => this.aiModelService.getAiModels(pageLink); + + this.config.cellActionDescriptors = this.configureCellActions(); + } + + resolve(_route: ActivatedRouteSnapshot): EntityTableConfig { + return this.config; + } + + private configureCellActions(): Array> { + return [ + { + name: this.translate.instant('action.edit'), + icon: 'edit', + isEnabled: () => true, + onAction: ($event, entity) => this.editModel(entity) + } + ]; + } + + private editModel(AIModel: AiModel): void { + this.addModel(AIModel, false).subscribe((res) => { + if (res) { + this.config.updateData(); + } + }); + } + + private addModel(AIModel: AiModel, isAdd = false): Observable { + return this.dialog.open(AIModelDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + AIModel + } + }).afterClosed().pipe(map(res => { + if (res) { + this.config.updateData(); + return res; + } else { + return null; + } + })); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.html b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.html new file mode 100644 index 0000000000..43f1dbf0db --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.html @@ -0,0 +1,21 @@ + +
+
ai-models.ai-models
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.scss new file mode 100644 index 0000000000..acb9e2dc87 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.scss @@ -0,0 +1,18 @@ +/** + * 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. + */ +:host{ + width: 100%; +} diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts new file mode 100644 index 0000000000..4fbc906b80 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts @@ -0,0 +1,33 @@ +/// +/// 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. +/// + +import { Component } from '@angular/core'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AiModel } from '@shared/models/ai-model.models'; + +@Component({ + selector: 'tb-ai-model-table-header', + templateUrl: './ai-model-table-header.component.html', + styleUrls: ['./ai-model-table-header.component.scss'] +}) +export class AiModelTableHeaderComponent extends EntityTableHeaderComponent { + + constructor(protected store: Store) { + super(store); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model.module.ts b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model.module.ts new file mode 100644 index 0000000000..bd20612e8a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model.module.ts @@ -0,0 +1,33 @@ +/// +/// 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. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { AiModelRoutingModule } from '@home/pages/ai-model/ai-model-routing.module'; +import { AiModelTableHeaderComponent } from '@home/pages/ai-model/ai-model-table-header.component'; + +@NgModule({ + declarations: [ + AiModelTableHeaderComponent + ], + imports: [ + CommonModule, + SharedModule, + AiModelRoutingModule + ] +}) +export class AiModelModule { } diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts index 39c442cc49..5bb3954cae 100644 --- a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts @@ -46,6 +46,7 @@ import { AccountModule } from '@home/pages/account/account.module'; import { ScadaSymbolModule } from '@home/pages/scada-symbol/scada-symbol.module'; import { GatewaysModule } from '@home/pages/gateways/gateways.module'; import { MobileModule } from '@home/pages/mobile/mobile.module'; +import { AiModelModule } from '@home/pages/ai-model/ai-model.module'; @NgModule({ exports: [ @@ -78,7 +79,8 @@ import { MobileModule } from '@home/pages/mobile/mobile.module'; UserModule, VcModule, AccountModule, - ScadaSymbolModule + ScadaSymbolModule, + AiModelModule, ] }) export class HomePagesModule { } diff --git a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html new file mode 100644 index 0000000000..1a8fae3b45 --- /dev/null +++ b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html @@ -0,0 +1,248 @@ + + +

{{ dialogTitle | translate }}

+ +
+ +
+ + +
+
+ +
+ + ai-models.name + + + + {{ 'ai-models.name-required' | translate }} + + + {{ 'ai-models.name-max-length' | translate }} + + +
+
+
+
ai-models.provider
+
+ + ai-models.ai-provider + + + {{providerTranslationMap.get(provider) | translate}} + + + +
+ @if (provider === aiProvider.GITHUB_MODELS) { + + ai-models.personal-access-token + + + + {{ 'ai-models.personal-access-token-required' | translate }} + + + } @else if (provider === aiProvider.GOOGLE_VERTEX_AI_GEMINI) { + + ai-models.project-id + + + {{ 'ai-models.project-id-required' | translate }} + + + + ai-models.location + + + {{ 'ai-models.location-required' | translate }} + + + + + } @else { + @if (provider === aiProvider.AZURE_OPENAI) { + + ai-models.endpoint + + + {{ 'ai-models.endpoint-required' | translate }} + + + + ai-models.service-version + + + } + + ai-models.api-key + + + + {{ 'ai-models.api-key-required' | translate }} + + + } +
+
+
+
+
ai-models.configuration
+
+
+ + +
+
+
+ {{ 'ai-models.temperature' | translate }} +
+ + + + warning + + +
+
+
+ {{ 'ai-models.top-p' | translate }} +
+ + + + warning + + +
+ + @if (provider === aiProvider.GOOGLE_AI_GEMINI || provider === aiProvider.GOOGLE_VERTEX_AI_GEMINI || provider === aiProvider.ANTHROPIC) { +
+
+ {{ 'ai-models.top-k' | translate }} +
+ + + + warning + + +
+ } + + @if (provider !== aiProvider.ANTHROPIC && provider !== aiProvider.AMAZON_BEDROCK) { +
+
+ {{ 'ai-models.presence-penalty' | translate }} +
+ + + +
+
+
+ {{ 'ai-models.frequency-penalty' | translate }} +
+ + + +
+ } + +
+
+ {{ 'ai-models.max-output-token' | translate }} +
+ + + + warning + + +
+
+
+
+ +
+
+ + + + +
diff --git a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.scss b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.scss new file mode 100644 index 0000000000..c55453c388 --- /dev/null +++ b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.scss @@ -0,0 +1,23 @@ +/** + * 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. + */ + +:host { + width: 850px; + height: 100%; + max-width: 100%; + max-height: 100vh; + display: grid; +} diff --git a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts new file mode 100644 index 0000000000..5dd63d3f5b --- /dev/null +++ b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts @@ -0,0 +1,168 @@ +/// +/// 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. +/// + +import { Component, Inject } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { Observable } from 'rxjs'; +import { StepperOrientation } from '@angular/cdk/stepper'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AiModel, AiProvider, AiProviderTranslations, AiProviderWithApiKey } from '@shared/models/ai-model.models'; +import { AiModelService } from '@core/http/ai-model.service'; +import { CheckConnectivityDialogComponent } from '@shared/components/ai-model/check-connectivity-dialog.component'; + +export interface AIModelDialogData { + AIModel?: AiModel; + isAdd?: boolean; +} + +@Component({ + selector: 'tb-ai-model-dialog', + templateUrl: './ai-model-dialog.component.html', + styleUrls: ['./ai-model-dialog.component.scss'] +}) +export class AIModelDialogComponent extends DialogComponent { + + readonly entityType = EntityType; + + selectedIndex = 0; + + dialogTitle = 'ai-models.ai-model'; + + stepperOrientation: Observable; + + aiProvider = AiProvider; + providerMap: AiProvider[] = Object.keys(AiProvider) as AiProvider[]; + providerTranslationMap = AiProviderTranslations; + + aiProviderWithApiKey: AiProvider[] = AiProviderWithApiKey; + + provider: AiProvider = AiProvider.OPENAI; + + aiModelForms: FormGroup; + + isAdd = false; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: AIModelDialogData, + private fb: FormBuilder, + private aiModelService: AiModelService, + private dialog: MatDialog) { + super(store, router, dialogRef); + + if (this.data.isAdd) { + this.isAdd = true; + } + + this.provider = this.data.AIModel ? this.data.AIModel.configuration.provider : AiProvider.OPENAI; + + this.aiModelForms = this.fb.group({ + name: [this.data.AIModel ? this.data.AIModel.name : '', [Validators.required, Validators.maxLength(255)]], + modelType: ['CHAT'], + configuration: this.fb.group({ + provider: [this.provider, [Validators.required]], + providerConfig: this.fb.group({ + apiKey: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.apiKey : '', [Validators.required]], + personalAccessToken: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.personalAccessToken : '', [Validators.required]], + endpoint: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.endpoint : '', [Validators.required]], + serviceVersion: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.serviceVersion : ''], + projectId: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.projectId : '', [Validators.required]], + location: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.location : '', [Validators.required]], + serviceAccountKey: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.serviceAccountKey : '', [Validators.required]], + serviceAccountKeyFileName: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.serviceAccountKeyFileName : '', [Validators.required]], + }), + modelConfig: this.fb.group({ + modelId: [this.data.AIModel ? this.data.AIModel.configuration.modelConfig?.modelId : '', [Validators.required]], + temperature: [this.data.AIModel ? this.data.AIModel.configuration.modelConfig?.temperature : null, [Validators.min(0)]], + topP: this.data.AIModel ? this.data.AIModel.configuration.modelConfig?.topP : [null, [Validators.min(0.1), Validators.max(1)]], + topK: [this.data.AIModel ? this.data.AIModel.configuration.modelConfig?.topK : null, [Validators.min(0)]], + frequencyPenalty: [this.data.AIModel ? this.data.AIModel.configuration.modelConfig?.frequencyPenalty : null], + presencePenalty: [this.data.AIModel ? this.data.AIModel.configuration.modelConfig?.presencePenalty : null], + maxOutputTokens: [this.data.AIModel ? this.data.AIModel.configuration.modelConfig?.maxOutputTokens : null, [Validators.min(1)]] + }) + }) + }); + + this.aiModelForms.get('configuration.provider').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((provider: AiProvider) => { + this.provider = provider; + this.aiModelForms.get('configuration.modelConfig').reset({}); + this.aiModelForms.get('configuration.providerConfig').reset({}); + this.updateValidation(provider); + }) + + this.updateValidation(this.provider); + } + + private updateValidation(provider: AiProvider) { + const providerConfig = this.aiModelForms.get('configuration.providerConfig'); + if (this.aiProviderWithApiKey.includes(provider)) { + providerConfig.get('apiKey').enable(); + } else { + providerConfig.get('apiKey').disable(); + } + if (provider === AiProvider.GITHUB_MODELS) { + providerConfig.get('personalAccessToken').enable(); + } else { + providerConfig.get('personalAccessToken').disable(); + } + if (provider === AiProvider.GOOGLE_VERTEX_AI_GEMINI) { + providerConfig.get('projectId').enable(); + providerConfig.get('location').enable(); + providerConfig.get('serviceAccountKey').enable(); + providerConfig.get('serviceAccountKeyFileName').enable(); + } else { + providerConfig.get('projectId').disable(); + providerConfig.get('location').disable(); + providerConfig.get('serviceAccountKey').disable(); + providerConfig.get('serviceAccountKeyFileName').disable(); + } + if (provider === AiProvider.AZURE_OPENAI) { + providerConfig.get('endpoint').enable(); + providerConfig.get('serviceVersion').enable(); + } else { + providerConfig.get('endpoint').disable(); + providerConfig.get('serviceVersion').disable(); + } + } + + cancel(): void { + this.dialogRef.close(null); + } + + checkConnectivity() { + return this.dialog.open(CheckConnectivityDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + AIModel: this.aiModelForms.value + } + }).afterClosed(); + } + + add(): void { + const aiModel = {...this.data.AIModel, ...this.aiModelForms.value} as AiModel; + this.aiModelService.saveAiModel(aiModel).subscribe(aiModel => this.dialogRef.close(aiModel)); + } +} diff --git a/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.html b/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.html new file mode 100644 index 0000000000..9ead874275 --- /dev/null +++ b/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.html @@ -0,0 +1,58 @@ + +
+

ai-models.check-connectivity

+ + +
+
+
+ + +
+
+ check_circle +
+ {{ "ai-models.check-connectivity-success" | translate }} +
+
+
+ cancel +
+
{{ "ai-models.check-connectivity-failed" | translate }}
+ + +
+
+
+
+ + +
diff --git a/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.scss b/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.scss new file mode 100644 index 0000000000..1c6195e246 --- /dev/null +++ b/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.scss @@ -0,0 +1,71 @@ +/** + * 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. + */ + +:host { + width: 560px; + height: 100%; + max-width: 100%; + max-height: 100vh; + display: grid; + + .connectivity-title { + font-size: 18px; + font-weight: 500; + margin: 0; + padding-left: 16px; + } + + .connection-status { + font-weight: 500; + letter-spacing: 0.25px; + text-align: center; + } + + .connection-icon { + height: 32px; + font-size: 32px; + width: 32px; + margin-bottom: 4px; + } + + .error_msg { + text-align: center; + margin-top: 8px; + font-size: 14px; + line-height: 130%; + letter-spacing: 0.25px; + opacity: 0.9; + } + + .success { + color: #198038; + } + + .error { + color: #D12730; + } + + ::ng-deep { + .json-editor { + .tb-json-object-toolbar { + display: none; + } + .tb-json-panel { + margin: 0; + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.ts b/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.ts new file mode 100644 index 0000000000..70896cdc6d --- /dev/null +++ b/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.ts @@ -0,0 +1,87 @@ +/// +/// 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. +/// + +import { Component, Inject } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { AiModel, AiModelWithUserMsg } from '@shared/models/ai-model.models'; +import { AiModelService } from '@core/http/ai-model.service'; + +export interface AIModelDialogData { + AIModel?: AiModel; +} + +@Component({ + selector: 'tb-check-connectivity-dialog', + templateUrl: './check-connectivity-dialog.component.html', + styleUrls: ['./check-connectivity-dialog.component.scss'] +}) +export class CheckConnectivityDialogComponent extends DialogComponent { + + showCheckSuccess = false; + checkErrMsg = ''; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: AIModelDialogData, + private aiModelService: AiModelService) { + super(store, router, dialogRef); + + if (this.data.AIModel) { + const aiModelWithMsg: AiModelWithUserMsg = { + userMessage: { + contents: [ + { + contentType: "TEXT", + text: "What is the capital of Ukraine?" + } + ] + }, + chatModel: { + modelType: "CHAT", + provider: this.data.AIModel.configuration.provider, + providerConfig: {...this.data.AIModel.configuration.providerConfig}, + modelConfig: { + modelId: this.data.AIModel.configuration.modelConfig.modelId, + maxRetries: 0, + timeoutSeconds: 20 + } + } + } + this.aiModelService.checkConnectivity(aiModelWithMsg, { + ignoreErrors: true, + ignoreLoading: true + }).subscribe({ + next: (result) => { + if (result.status === 'SUCCESS') { + this.showCheckSuccess = true; + } else { + this.checkErrMsg = JSON.parse(result.errorDetails); + } + }, + error: err => this.checkErrMsg = err.error.message + }); + } + } + + cancel(): void { + this.dialogRef.close(null); + } +} diff --git a/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.html b/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.html new file mode 100644 index 0000000000..d204013805 --- /dev/null +++ b/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.html @@ -0,0 +1,41 @@ + + + {{label}} + + + + {{errorText}} + + + + + + + diff --git a/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts b/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts new file mode 100644 index 0000000000..a5cbdb332d --- /dev/null +++ b/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts @@ -0,0 +1,161 @@ +/// +/// 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. +/// + +import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormControl, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { map, startWith, tap } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; +import { AiModelMap, AiProvider } from '@shared/models/ai-model.models'; + +@Component({ + selector: 'tb-models-list-autocomplete', + templateUrl: './models-list-autocomplete.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ModelsListAutocompleteComponent), + multi: true + } + ] +}) +export class ModelsListAutocompleteComponent implements ControlValueAccessor, OnInit, OnChanges { + + @ViewChild('nameInput', {static: true}) nameInput: ElementRef; + + @Input() + disabled: boolean; + + @Input() + @coerceBoolean() + required = false; + + @Input() + provider: AiProvider; + + @Input() + placeholderText: string = this.translate.instant('widget-config.set'); + + @Input() + subscriptSizing: SubscriptSizing = 'dynamic'; + + @Input() + appearance: MatFormFieldAppearance = 'outline'; + + @Input() + label: string; + + @Input() + errorText: string; + + selectionFormControl: FormControl; + modelValue: string | null; + + filteredOptions$: Observable>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: FormBuilder, + private translate: TranslateService) { + } + + ngOnInit() { + this.selectionFormControl = this.fb.control('', this.required ? [Validators.required] : []); + this.setupFilteredOptions(); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.provider && !changes.provider.isFirstChange()) { + this.setupFilteredOptions(); + this.selectionFormControl.setValue(null, {emitEvent: false}); + this.modelValue = null; + this.propagateChange(null); + } + } + + private setupFilteredOptions() { + this.filteredOptions$ = this.selectionFormControl.valueChanges.pipe( + startWith(''), + tap(value => this.updateView(value)), + map(value => { + const search = value ? value.toLowerCase() : ''; + const options = this.provider ? AiModelMap.get(this.provider) || [] : []; + return search ? options.filter(option => option.toLowerCase().includes(search)) : options; + }) + ); + } + + writeValue(option?: string): void { + this.searchText = ''; + this.modelValue = option ? option : null; + + if (option) { + this.selectionFormControl.patchValue(option, { emitEvent: false }); + this.dirty = true; + } else { + this.selectionFormControl.patchValue(null, { emitEvent: false }); + this.dirty = true; + } + } + + onFocus() { + if (this.dirty) { + this.selectionFormControl.updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + updateView(value: string) { + this.searchText = value ? value : ''; + if (this.modelValue !== value && value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.selectionFormControl.disable({emitEvent: false}); + } else { + this.selectionFormControl.enable({emitEvent: false}); + } + } + + clear() { + this.selectionFormControl.patchValue(null, {emitEvent: false}); + this.propagateChange(null); + this.modelValue = null; + setTimeout(() => { + this.nameInput.nativeElement.blur(); + this.nameInput.nativeElement.focus(); + }, 0); + } +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index ac8054283a..403502d56a 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -14,16 +14,7 @@ /// limitations under the License. /// -import { - Component, - ElementRef, - EventEmitter, - forwardRef, - Input, - OnInit, - Output, - ViewChild -} from '@angular/core'; +import { Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { firstValueFrom, merge, Observable, of, Subject } from 'rxjs'; @@ -300,6 +291,12 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit this.entityRequiredText = 'notification.notification-recipient-required'; this.notFoundEntities = 'notification.no-recipients-text'; break; + case EntityType.AI_MODEL_SETTINGS: + this.entityText = 'ai-models.ai-model'; + this.noEntitiesMatchingText = 'ai-models.no-model-matching'; + this.entityRequiredText = 'ai-models.model-required'; + this.notFoundEntities = 'ai-models.no-model-text'; + break; case AliasEntityType.CURRENT_CUSTOMER: this.entityText = 'customer.default-customer'; this.noEntitiesMatchingText = 'customer.no-customers-matching'; diff --git a/ui-ngx/src/app/shared/models/ai-model.models.ts b/ui-ngx/src/app/shared/models/ai-model.models.ts new file mode 100644 index 0000000000..5f0d845cac --- /dev/null +++ b/ui-ngx/src/app/shared/models/ai-model.models.ts @@ -0,0 +1,159 @@ +/// +/// 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. +/// + +import { BaseData } from '@shared/models/base-data'; +import { HasTenantId } from '@shared/models/entity.models'; +import { AiModelId } from '@shared/models/id/ai-model-id'; + +export interface AiModel extends Omit, 'label'>, HasTenantId { + modelType: string; + configuration: { + provider: AiProvider + providerConfig: { + apiKey?: string; + personalAccessToken?: string; + endpoint?: string; + serviceVersion?: string; + projectId?: string; + location?: string; + serviceAccountKey?: string; + serviceAccountKeyFileName?: string + }; + modelConfig: { + modelId: string; + temperature?: number | null; + topP?: number; + topK?: number; + frequencyPenalty?: number; + presencePenalty?: number; + maxOutputTokens?: number; + } + } +} + +export enum AiProvider { + OPENAI = 'OPENAI', + AZURE_OPENAI = 'AZURE_OPENAI', + GOOGLE_AI_GEMINI = 'GOOGLE_AI_GEMINI', + GOOGLE_VERTEX_AI_GEMINI = 'GOOGLE_VERTEX_AI_GEMINI', + MISTRAL_AI = 'MISTRAL_AI', + ANTHROPIC = 'ANTHROPIC', + AMAZON_BEDROCK = 'AMAZON_BEDROCK', + GITHUB_MODELS = 'GITHUB_MODELS' +} + +export const AiProviderWithApiKey: AiProvider[] = [ + AiProvider.OPENAI, + AiProvider.AZURE_OPENAI, + AiProvider.GOOGLE_AI_GEMINI, + AiProvider.MISTRAL_AI, + AiProvider.ANTHROPIC, + AiProvider.AMAZON_BEDROCK +] + +export const AiProviderTranslations = new Map( + [ + [AiProvider.OPENAI , 'ai-models.ai-providers.openai'], + [AiProvider.AZURE_OPENAI , 'ai-models.ai-providers.azure-openai'], + [AiProvider.GOOGLE_AI_GEMINI , 'ai-models.ai-providers.google-ai-gemini'], + [AiProvider.GOOGLE_VERTEX_AI_GEMINI , 'ai-models.ai-providers.google-vertex-ai-gemini'], + [AiProvider.MISTRAL_AI , 'ai-models.ai-providers.mistral-ai'], + [AiProvider.ANTHROPIC , 'ai-models.ai-providers.anthropic'], + [AiProvider.AMAZON_BEDROCK , 'ai-models.ai-providers.amazon-bedrock'], + [AiProvider.GITHUB_MODELS , 'ai-models.ai-providers.github-models'] + ] +); + +export const AiModelMap = new Map( + [ + [AiProvider.OPENAI , [ + 'o4-mini', + 'o3-pro', + 'o3', + 'o3-mini', + 'o1', + 'gpt-4.1', + 'gpt-4.1-mini', + 'gpt-4.1-nano', + 'gpt-4o', + 'gpt-4o-mini' + ]], + [AiProvider.AZURE_OPENAI , []], + [AiProvider.GOOGLE_AI_GEMINI , [ + 'gemini-2.5-pro', + 'gemini-2.5-flash', + 'gemini-2.0-flash', + 'gemini-2.0-flash-lite', + ]], + [AiProvider.GOOGLE_VERTEX_AI_GEMINI , [ + 'gemini-2.5-pro', + 'gemini-2.5-flash', + 'gemini-2.0-flash', + 'gemini-2.0-flash-lite', + ]], + [AiProvider.MISTRAL_AI , [ + 'magistral-medium-latest', + 'magistral-small-latest', + 'mistral-large-latest', + 'mistral-medium-latest', + 'mistral-small-latest', + 'pixtral-large-latest', + 'ministral-8b-latest', + 'ministral-3b-latest', + 'open-mistral-nemo' + ]], + [AiProvider.ANTHROPIC , [ + 'claude-opus-4-0', + 'claude-sonnet-4-0', + 'claude-3-7-sonnet-latest', + 'claude-3-5-sonnet-latest', + 'claude-3-5-haiku-latest' + ]], + [AiProvider.AMAZON_BEDROCK , []], + [AiProvider.GITHUB_MODELS , []] + ] +); + +export interface AiModelWithUserMsg { + userMessage: { + contents: Array<{contentType: string; text: string}>; + } + chatModel: { + modelType: string; + provider: AiProvider + providerConfig: { + apiKey?: string; + personalAccessToken?: string; + endpoint?: string; + serviceVersion?: string; + projectId?: string; + location?: string; + serviceAccountKey?: string; + serviceAccountKeyFileName?: string + }; + modelConfig: { + modelId: string; + maxRetries: number; + timeoutSeconds: number; + } + } +} + + +export interface CheckConnectivityResult { + status: string; + errorDetails: string; +} diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index fbb3e73216..3406412324 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -200,6 +200,7 @@ export const HelpLinks = { mobileBundle: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/mobile-center/`, mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`, calculatedField: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/calculated-fields/`, + aiModels: `${helpBaseUrl}/docs${docPlatformPrefix}/ai-models`, timewindowSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/dashboards/#time-window`, trendzSettings: `${helpBaseUrl}/docs/trendz/` } diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 02bae1576f..fa94c74041 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -496,8 +496,13 @@ export const entityTypeTranslations = new MapAre you sure you want to leave this page?", @@ -2555,7 +2625,8 @@ "type-current-user-owner": "Current User Owner", "type-calculated-field": "Calculated field", "type-calculated-fields": "Calculated fields", - "type-ai-model-settings": "AI model settings", + "type-ai-model": "AI model", + "type-ai-models": "AI models", "type-widgets-bundle": "Widgets bundle", "type-widgets-bundles": "Widgets bundles", "list-of-widgets-bundles": "{ count, plural, =1 {One widgets bundle} other {List of # widget bundles} }", @@ -5356,6 +5427,30 @@ "html-text-description": "Allows you to use HTML tags for formatting, links and images in your mai body.", "dynamic-text-description": "Allows to use Plain Text or HTML body type dynamically based on templatization feature.", "after-template-evaluation-hint": "After template evaluation value should be true for HTML, and false for Plain text." + }, + "ai": { + "ai-model": "AI model", + "ai-model-hint": "Select the pre-configured AI model to process requests sent by this rule node, or use \"Create new\" to configure a new one.", + "prompt-settings": "Prompt settings", + "prompt-settings-hint": "Configure the instructions for the AI. The optional system prompt sets the AI's general role and constraints, while the user prompt defines the specific task to perform. Both fields also support rule node templates to include dynamic data (e.g., use $[*] to access the entire message body or ${*} to access all metadata).", + "system-prompt": "System prompt", + "system-prompt-max-length": "System prompt must be 10000 characters or less.", + "system-prompt-blank": "System prompt must not be blank.", + "user-prompt": "User prompt", + "user-prompt-required": "User prompt is required.", + "user-prompt-max-length": "User prompt must be 10000 characters or less.", + "user-prompt-blank": "User prompt must not be blank.", + "response-format": "Response format", + "response-format-hint": "The model is required to generate a JSON that matches the specific structure and data types defined in the provided schema. If the output is not a valid JSON object, it will be automatically wrapped within a JSON object under the \"response\" key.", + "response-text": "Text", + "response-json": "JSON", + "response-json-schema": "JSON Schema", + "response-json-schema-required": "JSON Schema is required", + "advanced-settings": "Advanced settings", + "timeout": "Timeout", + "timeout-hint": "Maximum time to wait for a response \nfrom the AI model before the request is terminated.", + "timeout-required": "Timeout is required", + "timeout-validation": "Must be from 1 second to 10 minutes." } }, "timezone": { diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index e516810895..9896e16013 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -819,4 +819,34 @@ } } } + + .tb-form-panel.outlined { + --mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12); + --mdc-outlined-text-field-container-shape: 6px; + + box-shadow: none; + gap: 0; + padding-bottom: 0; + + &:not(.stroked) { + border-radius: 0; + } + + &:not(.mat-padding,.padding) { + padding: 0; + } + + .tb-form-panel-title { + margin-bottom: 16px; + } + + .tb-form-row { + height: 56px; + margin-bottom: 22px; + &.disabled { + border-color: var(--mdc-outlined-text-field-disabled-outline-color); + color: var(--mdc-outlined-text-field-disabled-input-text-color); + } + } + } } From 7c29d2a5d7a2a50f9e8daeb9ed29dca6758a3632 Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Tue, 8 Jul 2025 13:23:06 +0300 Subject: [PATCH 156/435] Added borders in a preview mode --- .../action/custom-action-pretty-editor.component.scss | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss index 6db246723f..2562d6c994 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss @@ -19,8 +19,10 @@ padding: 8px; background-color: #fff; - .tb-custom-action-editor-container { - border: none; + .css-panel, .html-panel{ + border-color: #c0c0c0; + border-width: 0 1px 1px 1px; + border-style: solid; } .tb-js-func-toolbar{ @@ -35,8 +37,7 @@ } .tb-js-func-panel { - border: none; - border-top: 1px solid #c0c0c0; + border: 1px solid #c0c0c0; } } @@ -72,7 +73,7 @@ padding: 8px 0 25px; } - .tb-custom-action-editor-container { + .css-panel, .html-panel{ border: none; border-bottom: 1px solid #c0c0c0; } From c6497281c36a1f07c96c8eaf2010bea2e7a4e46a Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 8 Jul 2025 13:24:27 +0300 Subject: [PATCH 157/435] Fix upgrade for ota_package --- application/src/main/data/upgrade/basic/schema_update.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index c959cfd6c1..160217fc4f 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -19,7 +19,7 @@ ALTER TABLE ota_package ADD COLUMN IF NOT EXISTS external_id uuid; ALTER TABLE ota_package - ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id); + ADD CONSTRAINT IF NOT EXISTS ota_package_external_id_unq_key UNIQUE (tenant_id, external_id); -- UPDATE OTA PACKAGE EXTERNAL ID END From a691ff126236d2bc3110f235d39b2e85701552e8 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 8 Jul 2025 14:28:42 +0300 Subject: [PATCH 158/435] Fix upgrade for ota_package --- .../src/main/data/upgrade/basic/schema_update.sql | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 160217fc4f..2c6f6a9ca0 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -18,8 +18,16 @@ ALTER TABLE ota_package ADD COLUMN IF NOT EXISTS external_id uuid; -ALTER TABLE ota_package - ADD CONSTRAINT IF NOT EXISTS ota_package_external_id_unq_key UNIQUE (tenant_id, external_id); + +DO +$$ + BEGIN + IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'ota_package_external_id_unq_key') THEN + ALTER TABLE ota_package + ADD CONSTRAINT ota_package_external_id_unq_key (tenant_id, external_id); + END IF; + END; +$$; -- UPDATE OTA PACKAGE EXTERNAL ID END From 2055dc83befd08c0ec4384f2bc80a6817d8db26d Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 8 Jul 2025 14:29:55 +0300 Subject: [PATCH 159/435] Cleanup script engine classes --- .../script/RuleNodeJsScriptEngine.java | 100 ++++++++-------- .../service/script/RuleNodeScriptEngine.java | 66 ++++------- .../script/RuleNodeTbelScriptEngine.java | 112 +++++++++--------- 3 files changed, 123 insertions(+), 155 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java index b7490af487..e0b8351ff6 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java @@ -17,18 +17,16 @@ package org.thingsboard.server.service.script; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.RuleNodeScriptFactory; +import org.thingsboard.script.api.TbScriptException; import org.thingsboard.script.api.js.JsInvokeService; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import javax.script.ScriptException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -36,8 +34,6 @@ import java.util.List; import java.util.Map; import java.util.Set; - -@Slf4j public class RuleNodeJsScriptEngine extends RuleNodeScriptEngine { public RuleNodeJsScriptEngine(TenantId tenantId, JsInvokeService scriptInvokeService, String script, String... argNames) { @@ -45,87 +41,81 @@ public class RuleNodeJsScriptEngine extends RuleNodeScriptEngine executeJsonAsync(TbMsg msg) { - return executeScriptAsync(msg); + protected Object[] prepareArgs(TbMsg msg) { + String[] args = new String[3]; + if (msg.getData() != null) { + args[0] = msg.getData(); + } else { + args[0] = ""; + } + args[1] = JacksonUtil.toString(msg.getMetaData().getData()); + args[2] = msg.getType(); + return args; } @Override - protected ListenableFuture> executeUpdateTransform(TbMsg msg, JsonNode json) { + protected List executeUpdateTransform(TbMsg msg, JsonNode json) { if (json.isObject()) { - return Futures.immediateFuture(Collections.singletonList(unbindMsg(json, msg))); + return Collections.singletonList(unbindMsg(json, msg)); } else if (json.isArray()) { List res = new ArrayList<>(json.size()); json.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg))); - return Futures.immediateFuture(res); + return res; } - log.warn("Wrong result type: {}", json.getNodeType()); - return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType())); + throw wrongResultType(json); } @Override - protected ListenableFuture executeGenerateTransform(TbMsg prevMsg, JsonNode result) { + protected TbMsg executeGenerateTransform(TbMsg prevMsg, JsonNode result) { if (!result.isObject()) { - log.warn("Wrong result type: {}", result.getNodeType()); - Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType())); - } - return Futures.immediateFuture(unbindMsg(result, prevMsg)); - } - - @Override - protected JsonNode convertResult(Object result) { - return JacksonUtil.toJsonNode(result != null ? result.toString() : null); - } - - @Override - protected ListenableFuture executeToStringTransform(JsonNode result) { - if (result.isTextual()) { - return Futures.immediateFuture(result.asText()); + throw wrongResultType(result); } - log.warn("Wrong result type: {}", result.getNodeType()); - return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType())); + return unbindMsg(result, prevMsg); } @Override - protected ListenableFuture executeFilterTransform(JsonNode json) { + protected boolean executeFilterTransform(JsonNode json) { if (json.isBoolean()) { - return Futures.immediateFuture(json.asBoolean()); + return json.asBoolean(); } - log.warn("Wrong result type: {}", json.getNodeType()); - return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType())); + throw wrongResultType(json); } @Override - protected ListenableFuture> executeSwitchTransform(JsonNode result) { + protected Set executeSwitchTransform(JsonNode result) { if (result.isTextual()) { - return Futures.immediateFuture(Collections.singleton(result.asText())); + return Collections.singleton(result.asText()); } if (result.isArray()) { Set nextStates = new HashSet<>(); for (JsonNode val : result) { if (!val.isTextual()) { - log.warn("Wrong result type: {}", val.getNodeType()); - return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + val.getNodeType())); + throw wrongResultType(val); } else { nextStates.add(val.asText()); } } - return Futures.immediateFuture(nextStates); + return nextStates; } - log.warn("Wrong result type: {}", result.getNodeType()); - return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType())); + throw wrongResultType(result); } @Override - protected Object[] prepareArgs(TbMsg msg) { - String[] args = new String[3]; - if (msg.getData() != null) { - args[0] = msg.getData(); - } else { - args[0] = ""; + public ListenableFuture executeJsonAsync(TbMsg msg) { + return executeScriptAsync(msg); + } + + @Override + protected String executeToStringTransform(JsonNode result) { + if (result.isTextual()) { + return result.asText(); } - args[1] = JacksonUtil.toString(msg.getMetaData().getData()); - args[2] = msg.getType(); - return args; + throw wrongResultType(result); + } + + @Override + protected JsonNode convertResult(Object result) { + return JacksonUtil.toJsonNode(result != null ? result.toString() : null); } private static TbMsg unbindMsg(JsonNode msgData, TbMsg msg) { @@ -138,19 +128,23 @@ public class RuleNodeJsScriptEngine extends RuleNodeScriptEngine() { - }); + metadata = JacksonUtil.convertValue(msgMetadata, new TypeReference<>() {}); } if (msgData.has(RuleNodeScriptFactory.MSG_TYPE)) { messageType = msgData.get(RuleNodeScriptFactory.MSG_TYPE).asText(); } String newData = data != null ? data : msg.getData(); TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData().copy(); - String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType(); + String newMessageType = StringUtils.isNotEmpty(messageType) ? messageType : msg.getType(); return msg.transform() .type(newMessageType) .metaData(newMetadata) .data(newData) .build(); } + + private TbScriptException wrongResultType(JsonNode result) { + return new TbScriptException(scriptId, TbScriptException.ErrorCode.RUNTIME, null, new ClassCastException("Wrong result type: " + result.getNodeType())); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java index d99f1654f3..ec9c2fd983 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java @@ -17,7 +17,6 @@ package org.thingsboard.server.service.script; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.script.api.ScriptInvokeService; @@ -27,25 +26,26 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbMsg; -import javax.script.ScriptException; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + @Slf4j public abstract class RuleNodeScriptEngine implements ScriptEngine { private final T scriptInvokeService; - private final UUID scriptId; + protected final UUID scriptId; private final TenantId tenantId; public RuleNodeScriptEngine(TenantId tenantId, T scriptInvokeService, String script, String... argNames) { this.tenantId = tenantId; this.scriptInvokeService = scriptInvokeService; try { - this.scriptId = this.scriptInvokeService.eval(tenantId, ScriptType.RULE_NODE_SCRIPT, script, argNames).get(); + scriptId = this.scriptInvokeService.eval(tenantId, ScriptType.RULE_NODE_SCRIPT, script, argNames).get(); } catch (Exception e) { Throwable t = e; if (e instanceof ExecutionException) { @@ -63,73 +63,53 @@ public abstract class RuleNodeScriptEngine imp @Override public ListenableFuture> executeUpdateAsync(TbMsg msg) { ListenableFuture result = executeScriptAsync(msg); - return Futures.transformAsync(result, - json -> executeUpdateTransform(msg, json), - MoreExecutors.directExecutor()); + return Futures.transform(result, json -> executeUpdateTransform(msg, json), directExecutor()); } - protected abstract ListenableFuture> executeUpdateTransform(TbMsg msg, R result); + protected abstract List executeUpdateTransform(TbMsg msg, R result); @Override public ListenableFuture executeGenerateAsync(TbMsg prevMsg) { - return Futures.transformAsync(executeScriptAsync(prevMsg), - result -> executeGenerateTransform(prevMsg, result), - MoreExecutors.directExecutor()); + return Futures.transform(executeScriptAsync(prevMsg), result -> executeGenerateTransform(prevMsg, result), directExecutor()); } - protected abstract ListenableFuture executeGenerateTransform(TbMsg prevMsg, R result); - - @Override - public ListenableFuture executeToStringAsync(TbMsg msg) { - return Futures.transformAsync(executeScriptAsync(msg), this::executeToStringTransform, MoreExecutors.directExecutor()); - } + protected abstract TbMsg executeGenerateTransform(TbMsg prevMsg, R result); @Override public ListenableFuture executeFilterAsync(TbMsg msg) { - return Futures.transformAsync(executeScriptAsync(msg), - this::executeFilterTransform, - MoreExecutors.directExecutor()); + return Futures.transform(executeScriptAsync(msg), this::executeFilterTransform, directExecutor()); } - protected abstract ListenableFuture executeToStringTransform(R result); + protected abstract boolean executeFilterTransform(R result); - protected abstract ListenableFuture executeFilterTransform(R result); + @Override + public ListenableFuture> executeSwitchAsync(TbMsg msg) { + return Futures.transform(executeScriptAsync(msg), this::executeSwitchTransform, directExecutor()); // usually runs on a callbackExecutor + } - protected abstract ListenableFuture> executeSwitchTransform(R result); + protected abstract Set executeSwitchTransform(R result); @Override - public ListenableFuture> executeSwitchAsync(TbMsg msg) { - return Futures.transformAsync(executeScriptAsync(msg), - this::executeSwitchTransform, - MoreExecutors.directExecutor()); //usually runs in a callbackExecutor + public ListenableFuture executeToStringAsync(TbMsg msg) { + return Futures.transform(executeScriptAsync(msg), this::executeToStringTransform, directExecutor()); } + protected abstract String executeToStringTransform(R result); + ListenableFuture executeScriptAsync(TbMsg msg) { log.trace("execute script async, msg {}", msg); Object[] inArgs = prepareArgs(msg); return executeScriptAsync(msg.getCustomerId(), inArgs[0], inArgs[1], inArgs[2]); } - ListenableFuture executeScriptAsync(CustomerId customerId, Object... args) { - return Futures.transformAsync(scriptInvokeService.invokeScript(tenantId, customerId, this.scriptId, args), - o -> { - try { - return Futures.immediateFuture(convertResult(o)); - } catch (Exception e) { - if (e.getCause() instanceof ScriptException) { - return Futures.immediateFailedFuture(e.getCause()); - } else if (e.getCause() instanceof RuntimeException) { - return Futures.immediateFailedFuture(new ScriptException(e.getCause().getMessage())); - } else { - return Futures.immediateFailedFuture(new ScriptException(e)); - } - } - }, MoreExecutors.directExecutor()); + private ListenableFuture executeScriptAsync(CustomerId customerId, Object... args) { + return Futures.transform(scriptInvokeService.invokeScript(tenantId, customerId, scriptId, args), this::convertResult, directExecutor()); } public void destroy() { - scriptInvokeService.release(this.scriptId); + scriptInvokeService.release(scriptId); } protected abstract R convertResult(Object result); + } diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java index 5e197d0e5d..991be63f81 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java @@ -19,17 +19,15 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.RuleNodeScriptFactory; +import org.thingsboard.script.api.TbScriptException; import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import javax.script.ScriptException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -40,8 +38,8 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; -@Slf4j public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine { public RuleNodeTbelScriptEngine(TenantId tenantId, TbelInvokeService scriptInvokeService, String script, String... argNames) { @@ -49,70 +47,74 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine executeFilterTransform(Object result) { - if (result instanceof Boolean) { - return Futures.immediateFuture((Boolean) result); + protected Object[] prepareArgs(TbMsg msg) { + Object[] args = new Object[3]; + if (msg.getData() != null) { + args[0] = JacksonUtil.fromString(msg.getData(), Object.class); + } else { + args[0] = new HashMap<>(); } - return wrongResultType(result); + args[1] = new HashMap<>(msg.getMetaData().getData()); + args[2] = msg.getType(); + return args; } @Override - protected ListenableFuture> executeUpdateTransform(TbMsg msg, Object result) { - if (result instanceof Map) { - return Futures.immediateFuture(Collections.singletonList(unbindMsg((Map) result, msg))); - } else if (result instanceof Collection) { - List res = new ArrayList<>(); - for (Object resObject : (Collection) result) { - if (resObject instanceof Map) { - res.add(unbindMsg((Map) resObject, msg)); + protected List executeUpdateTransform(TbMsg msg, Object result) { + if (result instanceof Map msgData) { + return Collections.singletonList(unbindMsg(msgData, msg)); + } else if (result instanceof Collection resultCollection) { + List res = new ArrayList<>(resultCollection.size()); + for (Object resObject : resultCollection) { + if (resObject instanceof Map msgData) { + res.add(unbindMsg(msgData, msg)); } else { - return wrongResultType(resObject); + throw wrongResultType(resObject); } } - return Futures.immediateFuture(res); + return res; } - return wrongResultType(result); + throw wrongResultType(result); } @Override - protected ListenableFuture executeGenerateTransform(TbMsg prevMsg, Object result) { - if (result instanceof Map) { - return Futures.immediateFuture(unbindMsg((Map) result, prevMsg)); + protected TbMsg executeGenerateTransform(TbMsg prevMsg, Object result) { + if (result instanceof Map msgData) { + return unbindMsg(msgData, prevMsg); } - return wrongResultType(result); + throw wrongResultType(result); } @Override - protected ListenableFuture executeToStringTransform(Object result) { - if (result instanceof String) { - return Futures.immediateFuture((String) result); - } else { - return Futures.immediateFuture(JacksonUtil.toString(result)); + protected boolean executeFilterTransform(Object result) { + if (result instanceof Boolean b) { + return b; } + throw wrongResultType(result); } @Override - protected ListenableFuture> executeSwitchTransform(Object result) { - if (result instanceof String) { - return Futures.immediateFuture(Collections.singleton((String) result)); - } else if (result instanceof Collection) { - Set res = new HashSet<>(); - for (Object resObject : (Collection) result) { - if (resObject instanceof String) { - res.add((String) resObject); + protected Set executeSwitchTransform(Object result) { + if (result instanceof String str) { + return Collections.singleton(str); + } + if (result instanceof Collection resultCollection) { + Set res = new HashSet<>(resultCollection.size()); + for (Object resObject : resultCollection) { + if (resObject instanceof String str) { + res.add(str); } else { - return wrongResultType(resObject); + throw wrongResultType(resObject); } } - return Futures.immediateFuture(res); + return res; } - return wrongResultType(result); + throw wrongResultType(result); } @Override public ListenableFuture executeJsonAsync(TbMsg msg) { - return Futures.transform(executeScriptAsync(msg), JacksonUtil::valueToTree, MoreExecutors.directExecutor()); - + return Futures.transform(executeScriptAsync(msg), JacksonUtil::valueToTree, directExecutor()); } @Override @@ -121,16 +123,8 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine(); - } - args[1] = new HashMap<>(msg.getMetaData().getData()); - args[2] = msg.getType(); - return args; + protected String executeToStringTransform(Object result) { + return result instanceof String str ? str : JacksonUtil.toString(result); } private static TbMsg unbindMsg(Map msgData, TbMsg msg) { @@ -142,12 +136,12 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine) msgMetadataObj).entrySet().stream().filter(e -> e.getValue() != null) + if (msgMetadataObj instanceof Map msgMetadataObjAsMap) { + metadata = msgMetadataObjAsMap.entrySet().stream() + .filter(e -> e.getValue() != null) .collect(Collectors.toMap(e -> e.getKey().toString(), e -> e.getValue().toString())); } else { - metadata = JacksonUtil.convertValue(msgMetadataObj, new TypeReference<>() { - }); + metadata = JacksonUtil.convertValue(msgMetadataObj, new TypeReference<>() {}); } } if (msgData.containsKey(RuleNodeScriptFactory.MSG_TYPE)) { @@ -155,7 +149,7 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine ListenableFuture wrongResultType(Object result) { + private TbScriptException wrongResultType(Object result) { String className = toClassName(result); - log.warn("Wrong result type: {}", className); - return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + className)); + return new TbScriptException(scriptId, TbScriptException.ErrorCode.RUNTIME, null, new ClassCastException("Wrong result type: " + className)); } private static String toClassName(Object result) { return result != null ? result.getClass().getSimpleName() : "null"; } + } From 349e7402a47c78ead512aa59f1c6fcd37f528460 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 8 Jul 2025 14:33:25 +0300 Subject: [PATCH 160/435] Small refactoring --- application/src/main/data/upgrade/basic/schema_update.sql | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 2c6f6a9ca0..22cac14829 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -23,8 +23,7 @@ DO $$ BEGIN IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'ota_package_external_id_unq_key') THEN - ALTER TABLE ota_package - ADD CONSTRAINT ota_package_external_id_unq_key (tenant_id, external_id); + ALTER TABLE ota_package ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id); END IF; END; $$; From a0e8b014297c0569644ba3bef67ee794731c225f Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 8 Jul 2025 15:32:24 +0300 Subject: [PATCH 161/435] Add tests for compilation errors when evaluating scripts --- .../script/NashornJsInvokeServiceTest.java | 22 +++++++++ .../script/RemoteJsInvokeServiceTest.java | 45 ++++++++++++++--- .../service/script/TbelInvokeServiceTest.java | 22 +++++++++ .../script/api/TbScriptExceptionTest.java | 49 +++++++++++++++++++ 4 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 common/script/script-api/src/test/java/org/thingsboard/script/api/TbScriptExceptionTest.java diff --git a/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java index 0942cef75d..b8ab48b38d 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java @@ -25,11 +25,13 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.TbStopWatch; import org.thingsboard.script.api.ScriptType; +import org.thingsboard.script.api.TbScriptException; import org.thingsboard.script.api.js.NashornJsInvokeService; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; +import javax.script.ScriptException; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -39,6 +41,7 @@ import java.util.concurrent.TimeoutException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; @@ -59,6 +62,25 @@ class NashornJsInvokeServiceTest extends AbstractControllerTest { @Value("${js.local.max_errors}") private int maxJsErrors; + @Test + void givenUncompilableScript_whenEvaluating_thenThrowsErrorWithCompilationErrorCode() { + // GIVEN + var uncompilableScript = "return msg.temperature?.value;"; + + // WHEN-THEN + assertThatThrownBy(() -> evalScript(uncompilableScript)) + .isInstanceOf(ExecutionException.class) + .cause() + .isInstanceOf(TbScriptException.class) + .asInstanceOf(type(TbScriptException.class)) + .satisfies(ex -> { + assertThat(ex.getScriptId()).isNotNull(); + assertThat(ex.getErrorCode()).isEqualTo(TbScriptException.ErrorCode.COMPILATION); + assertThat(ex.getBody()).contains(uncompilableScript); + assertThat(ex.getCause()).isInstanceOf(ScriptException.class); + }); + } + @Test void givenSimpleScriptTestPerformance() throws ExecutionException, InterruptedException { int iterations = 1000; diff --git a/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java index 363f21fa10..36990d9768 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java @@ -23,9 +23,9 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.script.api.ScriptType; +import org.thingsboard.script.api.TbScriptException; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.stats.DefaultStatsFactory; import org.thingsboard.server.common.stats.StatsCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.TbApiUsageReportClient; @@ -42,8 +42,11 @@ import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ExecutionException; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doAnswer; @@ -60,7 +63,6 @@ class RemoteJsInvokeServiceTest { private RemoteJsInvokeService remoteJsInvokeService; private TbQueueRequestTemplate, TbProtoQueueMsg> jsRequestTemplate; - @BeforeEach public void beforeEach() { TbApiUsageStateClient apiUsageStateClient = mock(TbApiUsageStateClient.class); @@ -74,7 +76,7 @@ class RemoteJsInvokeServiceTest { remoteJsInvokeService.requestTemplate = jsRequestTemplate; StatsFactory statsFactory = mock(StatsFactory.class); when(statsFactory.createStatsCounter(any(), any())).thenReturn(mock(StatsCounter.class)); - ReflectionTestUtils.setField(remoteJsInvokeService, "statsFactory",statsFactory); + ReflectionTestUtils.setField(remoteJsInvokeService, "statsFactory", statsFactory); remoteJsInvokeService.init(); } @@ -84,7 +86,36 @@ class RemoteJsInvokeServiceTest { } @Test - public void whenInvokingFunction_thenDoNotSendScriptBody() throws Exception { + void givenUncompilableScript_whenEvaluating_thenThrowsErrorWithCompilationErrorCode() { + // GIVEN + doAnswer(methodCall -> Futures.immediateFuture(new TbProtoJsQueueMsg<>(UUID.randomUUID(), RemoteJsResponse.newBuilder() + .setCompileResponse(JsInvokeProtos.JsCompileResponse.newBuilder() + .setSuccess(false) + .setErrorCode(JsInvokeProtos.JsInvokeErrorCode.COMPILATION_ERROR) + .setErrorDetails("SyntaxError: Unexpected token 'const'") + .setScriptHash(methodCall.>getArgument(0).getValue().getCompileRequest().getScriptHash()) + .build()) + .build()))) + .when(jsRequestTemplate).send(argThat(jsQueueMsg -> jsQueueMsg.getValue().hasCompileRequest())); + + var uncompilableScript = "let const = 'this is not allowed';"; + + // WHEN-THEN + assertThatThrownBy(() -> remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, uncompilableScript).get()) + .isInstanceOf(ExecutionException.class) + .cause() + .isInstanceOf(TbScriptException.class) + .asInstanceOf(type(TbScriptException.class)) + .satisfies(ex -> { + assertThat(ex.getScriptId()).isNotNull(); + assertThat(ex.getErrorCode()).isEqualTo(TbScriptException.ErrorCode.COMPILATION); + assertThat(ex.getBody()).contains(uncompilableScript); + assertThat(ex.getCause()).isInstanceOf(RuntimeException.class).hasMessage("SyntaxError: Unexpected token 'const'"); + }); + } + + @Test + void whenInvokingFunction_thenDoNotSendScriptBody() throws Exception { mockJsEvalResponse(); String scriptBody = "return { a: 'b'};"; UUID scriptId = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get(); @@ -110,7 +141,7 @@ class RemoteJsInvokeServiceTest { } @Test - public void whenInvokingFunctionAndRemoteJsExecutorRemovedScript_thenHandleNotFoundErrorAndMakeInvokeRequestWithScriptBody() throws Exception { + void whenInvokingFunctionAndRemoteJsExecutorRemovedScript_thenHandleNotFoundErrorAndMakeInvokeRequestWithScriptBody() throws Exception { mockJsEvalResponse(); String scriptBody = "return { a: 'b'};"; UUID scriptId = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get(); @@ -156,7 +187,7 @@ class RemoteJsInvokeServiceTest { } @Test - public void whenDoingEval_thenSaveScriptByHashOfTenantIdAndScriptBody() throws Exception { + void whenDoingEval_thenSaveScriptByHashOfTenantIdAndScriptBody() throws Exception { mockJsEvalResponse(); TenantId tenantId1 = TenantId.fromUUID(UUID.randomUUID()); @@ -187,7 +218,7 @@ class RemoteJsInvokeServiceTest { } @Test - public void whenReleasingScript_thenCheckForHashUsages() throws Exception { + void whenReleasingScript_thenCheckForHashUsages() throws Exception { mockJsEvalResponse(); String scriptBody = "return { a: 'b'};"; UUID scriptId1 = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get(); diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java index 732f31f044..fc66f806d7 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java @@ -20,10 +20,12 @@ import com.github.benmanes.caffeine.cache.Cache; import org.junit.Assert; import org.junit.Ignore; import org.junit.jupiter.api.Test; +import org.mvel2.CompileException; import org.springframework.beans.factory.annotation.Value; import org.springframework.test.context.TestPropertySource; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.script.api.TbScriptException; import org.thingsboard.script.api.tbel.TbelScript; import java.io.Serializable; @@ -37,6 +39,7 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.InstanceOfAssertFactories.type; @TestPropertySource(properties = { "tbel.max_script_body_size=100", @@ -50,6 +53,25 @@ class TbelInvokeServiceTest extends AbstractTbelInvokeTest { @Value("${tbel.max_errors}") private int maxJsErrors; + @Test + void givenUncompilableScript_whenEvaluating_thenThrowsErrorWithCompilationErrorCode() { + // GIVEN + var uncompilableScript = "return msg.property !== undefined;"; + + // WHEN-THEN + assertThatThrownBy(() -> evalScript(uncompilableScript)) + .isInstanceOf(ExecutionException.class) + .cause() + .isInstanceOf(TbScriptException.class) + .asInstanceOf(type(TbScriptException.class)) + .satisfies(ex -> { + assertThat(ex.getScriptId()).isNotNull(); + assertThat(ex.getErrorCode()).isEqualTo(TbScriptException.ErrorCode.COMPILATION); + assertThat(ex.getBody()).isEqualTo(uncompilableScript); + assertThat(ex.getCause()).isInstanceOf(CompileException.class); + }); + } + @Test void givenSimpleScriptTestPerformance() throws ExecutionException, InterruptedException { int iterations = 100000; diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/TbScriptExceptionTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/TbScriptExceptionTest.java new file mode 100644 index 0000000000..330b895504 --- /dev/null +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/TbScriptExceptionTest.java @@ -0,0 +1,49 @@ +/** + * 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.script.api; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class TbScriptExceptionTest { + + @Test + void givenCompilationError_whenCheckingIsUnrecoverable_thenReturnsTrue() { + // GIVEN + var exception = new TbScriptException(null, TbScriptException.ErrorCode.COMPILATION, null, null); + + // WHEN-THEN + assertThat(exception.isUnrecoverable()).isTrue(); + } + + @ParameterizedTest + @EnumSource( + value = TbScriptException.ErrorCode.class, + mode = EnumSource.Mode.EXCLUDE, + names = "COMPILATION" + ) + void givenRecoverableErrorCodes_whenCheckingIsUnrecoverable_thenReturnsFalse(TbScriptException.ErrorCode errorCode) { + // GIVEN + var exception = new TbScriptException(null, errorCode, null, null); + + // WHEN-THEN + assertThat(exception.isUnrecoverable()).isFalse(); + } + +} From 7860f4575f4668cc81c072c3bfcfc4cacbcff289 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 8 Jul 2025 17:46:18 +0300 Subject: [PATCH 162/435] UI: Add VC support --- ui-ngx/src/app/shared/models/ai-model.models.ts | 4 ++-- ui-ngx/src/app/shared/models/vc.models.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/shared/models/ai-model.models.ts b/ui-ngx/src/app/shared/models/ai-model.models.ts index 5f0d845cac..1f9c81bb1e 100644 --- a/ui-ngx/src/app/shared/models/ai-model.models.ts +++ b/ui-ngx/src/app/shared/models/ai-model.models.ts @@ -14,11 +14,11 @@ /// limitations under the License. /// -import { BaseData } from '@shared/models/base-data'; +import { BaseData, ExportableEntity } from '@shared/models/base-data'; import { HasTenantId } from '@shared/models/entity.models'; import { AiModelId } from '@shared/models/id/ai-model-id'; -export interface AiModel extends Omit, 'label'>, HasTenantId { +export interface AiModel extends Omit, 'label'>, HasTenantId, ExportableEntity { modelType: string; configuration: { provider: AiProvider diff --git a/ui-ngx/src/app/shared/models/vc.models.ts b/ui-ngx/src/app/shared/models/vc.models.ts index 3795518ffc..3ce5b6ae3f 100644 --- a/ui-ngx/src/app/shared/models/vc.models.ts +++ b/ui-ngx/src/app/shared/models/vc.models.ts @@ -35,14 +35,16 @@ export const exportableEntityTypes: Array = [ EntityType.TB_RESOURCE, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, - EntityType.NOTIFICATION_RULE + EntityType.NOTIFICATION_RULE, + EntityType.AI_MODEL_SETTINGS ]; export const entityTypesWithoutRelatedData: Set = new Set([ EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, - EntityType.TB_RESOURCE + EntityType.TB_RESOURCE, + EntityType.AI_MODEL_SETTINGS ]); export interface VersionCreateConfig { From 000c42963a53e89d4633f08ad2f13e909c274889 Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Wed, 9 Jul 2025 11:19:44 +0300 Subject: [PATCH 163/435] Blocks and gutter aligned, removed extra borders --- ...custom-action-pretty-editor.component.html | 2 +- ...custom-action-pretty-editor.component.scss | 73 +++++++------------ 2 files changed, 27 insertions(+), 48 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html index 3d2e69e210..3b2c14c539 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html @@ -43,7 +43,7 @@
-
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss index 2562d6c994..c9173bbc1a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss @@ -43,57 +43,48 @@ .tb-fullscreen-panel { .tb-custom-action-editor-container { - height: calc(100% - 40px); - } - - .right-panel { - padding-top: 8px; - padding-left: 3px; - } - - tb-js-func .tb-js-func-panel { - box-sizing: border-box; + height: 100%; } - .mat-mdc-tab-group { - .mat-mdc-tab-body-wrapper { - height: 100%; - - .mat-mdc-tab-body { - height: 100%; - - & > div { - height: 100%; - } - } - } + .css-panel, .html-panel{ + border: none; } - .right-panel{ - padding: 8px 0 25px; + .left-panel, .right-panel{ + height: calc(100% - 40px); } - .css-panel, .html-panel{ - border: none; - border-bottom: 1px solid #c0c0c0; + .right-panel { + padding: 8px 0 0; } .tb-js-func { + .tb-js-func-panel { + box-sizing: border-box; + } &.fill-height { .tb-js-func-toolbar{ padding: 0 5px; } - &:not(.tb-fullscreen) { - &.tb-hide-brackets { - padding-bottom: 15px; - } - } &.tb-hide-brackets { .tb-js-func-panel { border: none; border-top: 1px solid #c0c0c0; - border-bottom: 1px solid #c0c0c0; + } + } + } + } + + .mat-mdc-tab-group { + .mat-mdc-tab-body-wrapper { + height: 100%; + + .mat-mdc-tab-body { + height: 100%; + + & > div { + height: 100%; } } } @@ -124,7 +115,7 @@ .tb-split.tb-split-horizontal, .gutter.gutter-horizontal { float: left; - height: 100%; + height: calc(100% - 40px); } .tb-action-expand-button { @@ -135,20 +126,7 @@ &.tb-fullscreen-editor { position: relative; right: 0; - /* .mat-mdc-button { - .mat-icon { - margin-right: 5px; - } - } */ } - - /* .mat-mdc-button { - min-width: 36px; - padding: 0; - .mat-icon { - margin-right: 0; - } - } */ } .tb-custom-action-editor { @@ -159,3 +137,4 @@ } + From aaea558e79d6bb39e2b8f86b8a7704016b139787 Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Wed, 9 Jul 2025 11:33:47 +0300 Subject: [PATCH 164/435] Removed extra style --- .../common/action/custom-action-pretty-editor.component.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss index c9173bbc1a..f0e09589ea 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss @@ -35,10 +35,6 @@ padding-bottom: 0; } } - - .tb-js-func-panel { - border: 1px solid #c0c0c0; - } } .tb-fullscreen-panel { From b6b30dbba9d50d372aa60adf35f1b2038caa3f03 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 9 Jul 2025 15:37:52 +0300 Subject: [PATCH 165/435] AI rule node: rename AI model settings to AI model --- .../server/actors/ActorSystemContext.java | 8 +- .../actors/ruleChain/DefaultTbContext.java | 12 +- .../server/controller/AiModelController.java | 122 +++++++++- .../controller/AiModelSettingsController.java | 143 ------------ .../server/controller/BaseController.java | 22 +- .../controller/ControllerConstants.java | 2 +- ...elService.java => AiChatModelService.java} | 4 +- ...eImpl.java => AiChatModelServiceImpl.java} | 9 +- .../Langchain4jChatModelConfigurerImpl.java | 214 +++++++++--------- .../edge/EdgeEventSourcingListener.java | 4 +- .../edge/RelatedEdgesSourcingListener.java | 2 +- ...vice.java => DefaultTbAiModelService.java} | 36 +-- ...ingsService.java => TbAiModelService.java} | 8 +- .../service/security/permission/Resource.java | 2 +- .../permission/TenantAdminPermissions.java | 10 +- .../DefaultEntitiesExportImportService.java | 2 +- ...Service.java => AiModelExportService.java} | 8 +- ...Service.java => AiModelImportService.java} | 44 ++-- .../src/main/resources/thingsboard.yml | 8 +- ...ttingsService.java => AiModelService.java} | 18 +- .../server/common/data/CacheConstants.java | 4 +- .../server/common/data/EntityType.java | 4 +- .../ai/{AiModelSettings.java => AiModel.java} | 38 ++-- .../common/data/ai/dto/TbChatRequest.java | 4 +- .../server/common/data/ai/model/AiModel.java | 84 ------- .../common/data/ai/model/AiModelConfig.java | 60 ++++- .../data/ai/model/chat/AiChatModel.java | 41 ---- .../data/ai/model/chat/AiChatModelConfig.java | 15 +- ...java => AmazonBedrockChatModelConfig.java} | 21 +- ...del.java => AnthropicChatModelConfig.java} | 23 +- ...l.java => AzureOpenAiChatModelConfig.java} | 25 +- ....java => GitHubModelsChatModelConfig.java} | 25 +- ...ava => GoogleAiGeminiChatModelConfig.java} | 27 +-- ... GoogleVertexAiGeminiChatModelConfig.java} | 27 +-- .../chat/Langchain4jChatModelConfigurer.java | 16 +- ...del.java => MistralAiChatModelConfig.java} | 25 +- ...tModel.java => OpenAiChatModelConfig.java} | 25 +- ...{AiModelSettingsId.java => AiModelId.java} | 16 +- .../common/data/id/EntityIdFactory.java | 2 +- .../server/common/data/sync/JsonTbEntity.java | 4 +- common/proto/src/main/proto/queue.proto | 2 +- ...Event.java => AiModelCacheEvictEvent.java} | 16 +- ...ingsCacheKey.java => AiModelCacheKey.java} | 18 +- ...neCache.java => AiModelCaffeineCache.java} | 10 +- ...iModelSettingsDao.java => AiModelDao.java} | 14 +- ...RedisCache.java => AiModelRedisCache.java} | 10 +- .../server/dao/ai/AiModelServiceImpl.java | 149 ++++++++++++ .../dao/ai/AiModelSettingsServiceImpl.java | 149 ------------ .../dao/housekeeper/CleanUpService.java | 2 +- .../server/dao/model/ModelConstants.java | 10 +- ...SettingsEntity.java => AiModelEntity.java} | 54 ++--- ...lidator.java => AiModelDataValidator.java} | 36 +-- ...Repository.java => AiModelRepository.java} | 40 ++-- ...delSettingsDao.java => JpaAiModelDao.java} | 66 +++--- .../server/dao/tenant/TenantServiceImpl.java | 2 +- .../resources/sql/schema-entities-idx.sql | 2 +- .../main/resources/sql/schema-entities.sql | 6 +- ...java => RuleEngineAiChatModelService.java} | 5 +- .../rule/engine/api/TbContext.java | 6 +- .../thingsboard/rule/engine/ai/TbAiNode.java | 43 ++-- .../rule/engine/ai/TbAiNodeConfiguration.java | 4 +- .../rule/engine/util/TenantIdLoader.java | 6 +- .../rule/engine/util/TenantIdLoaderTest.java | 16 +- 63 files changed, 848 insertions(+), 982 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java rename application/src/main/java/org/thingsboard/server/service/ai/{AiModelService.java => AiChatModelService.java} (82%) rename application/src/main/java/org/thingsboard/server/service/ai/{AiModelServiceImpl.java => AiChatModelServiceImpl.java} (79%) rename application/src/main/java/org/thingsboard/server/service/entitiy/ai/{DefaultTbAiModelSettingsService.java => DefaultTbAiModelService.java} (54%) rename application/src/main/java/org/thingsboard/server/service/entitiy/ai/{TbAiModelSettingsService.java => TbAiModelService.java} (76%) rename application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/{AiModelSettingsExportService.java => AiModelExportService.java} (76%) rename application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/{AiModelSettingsImportService.java => AiModelImportService.java} (50%) rename common/dao-api/src/main/java/org/thingsboard/server/dao/ai/{AiModelSettingsService.java => AiModelService.java} (55%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/{AiModelSettings.java => AiModel.java} (67%) delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java rename common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/{AmazonBedrockChatModel.java => AmazonBedrockChatModelConfig.java} (75%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/{AnthropicChatModel.java => AnthropicChatModelConfig.java} (74%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/{AzureOpenAiChatModel.java => AzureOpenAiChatModelConfig.java} (72%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/{GitHubModelsChatModel.java => GitHubModelsChatModelConfig.java} (72%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/{GoogleAiGeminiChatModel.java => GoogleAiGeminiChatModelConfig.java} (71%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/{GoogleVertexAiGeminiChatModel.java => GoogleVertexAiGeminiChatModelConfig.java} (71%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/{MistralAiChatModel.java => MistralAiChatModelConfig.java} (72%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/{OpenAiChatModel.java => OpenAiChatModelConfig.java} (73%) rename common/data/src/main/java/org/thingsboard/server/common/data/id/{AiModelSettingsId.java => AiModelId.java} (72%) rename dao/src/main/java/org/thingsboard/server/dao/ai/{AiModelSettingsCacheEvictEvent.java => AiModelCacheEvictEvent.java} (58%) rename dao/src/main/java/org/thingsboard/server/dao/ai/{AiModelSettingsCacheKey.java => AiModelCacheKey.java} (68%) rename dao/src/main/java/org/thingsboard/server/dao/ai/{AiModelSettingsCaffeineCache.java => AiModelCaffeineCache.java} (75%) rename dao/src/main/java/org/thingsboard/server/dao/ai/{AiModelSettingsDao.java => AiModelDao.java} (60%) rename dao/src/main/java/org/thingsboard/server/dao/ai/{AiModelSettingsRedisCache.java => AiModelRedisCache.java} (71%) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java rename dao/src/main/java/org/thingsboard/server/dao/model/sql/{AiModelSettingsEntity.java => AiModelEntity.java} (62%) rename dao/src/main/java/org/thingsboard/server/dao/service/validator/{AiModelSettingsDataValidator.java => AiModelDataValidator.java} (51%) rename dao/src/main/java/org/thingsboard/server/dao/sql/ai/{AiModelSettingsRepository.java => AiModelRepository.java} (55%) rename dao/src/main/java/org/thingsboard/server/dao/sql/ai/{JpaAiModelSettingsDao.java => JpaAiModelDao.java} (50%) rename rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/{RuleEngineAiModelService.java => RuleEngineAiChatModelService.java} (83%) diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index cd4a88314b..ea46ce86eb 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -35,7 +35,7 @@ import org.thingsboard.rule.engine.api.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 diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index b4235c7262..6374e4016d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -28,7 +28,7 @@ import org.thingsboard.rule.engine.api.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 diff --git a/application/src/main/java/org/thingsboard/server/controller/AiModelController.java b/application/src/main/java/org/thingsboard/server/controller/AiModelController.java index c7dfce90bc..5a00e26f17 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AiModelController.java +++ b/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 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 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 sendChatRequest(@Valid @RequestBody TbChatRequest tbChatRequest) { ChatRequest langChainChatRequest = tbChatRequest.toLangChainChatRequest(); - AiChatModel chatModel = tbChatRequest.chatModel(); + AiChatModelConfig chatModelConfig = tbChatRequest.chatModelConfig(); - ListenableFuture future = aiModelService.sendChatRequestAsync(chatModel, langChainChatRequest) + ListenableFuture 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); } diff --git a/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java deleted file mode 100644 index b729d9f11e..0000000000 --- a/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java +++ /dev/null @@ -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 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 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); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 0ccd01bb4f..61dcd76b32 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/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) 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 emptyId(EntityType entityType) { diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index 20e350f0b8..a87864726b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/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."; diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiModelService.java b/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelService.java similarity index 82% rename from application/src/main/java/org/thingsboard/server/service/ai/AiModelService.java rename to application/src/main/java/org/thingsboard/server/service/ai/AiChatModelService.java index 196a53ce3a..9e00c8ddfd 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/AiModelService.java +++ b/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 {} diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiModelServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java similarity index 79% rename from application/src/main/java/org/thingsboard/server/service/ai/AiModelServiceImpl.java rename to application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java index 877f80fb26..d6252f57a6 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/AiModelServiceImpl.java +++ b/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 > FluentFuture sendChatRequestAsync(AiChatModel chatModel, ChatRequest chatRequest) { - ChatModel lc4jChatModel = chatModel.configure(chatModelConfigurer); - return aiRequestsExecutor.sendChatRequestAsync(lc4jChatModel, chatRequest); + public > FluentFuture sendChatRequestAsync(AiChatModelConfig chatModelConfig, ChatRequest chatRequest) { + ChatModel langChainChatModel = chatModelConfig.configure(chatModelConfigurer); + return aiRequestsExecutor.sendChatRequestAsync(langChainChatModel, chatRequest); } } diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index 10e327b48c..69dd98f47f 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/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(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index 8bbed875fe..e31bbe21ac 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -112,7 +112,7 @@ public class EdgeEventSourcingListener { return; } try { - if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_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) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java index 5fca13380c..8a111e4d9d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java @@ -68,7 +68,7 @@ public class RelatedEdgesSourcingListener { @TransactionalEventListener( fallbackExecution = true, - condition = "#event.entityId.getEntityType() != T(org.thingsboard.server.common.data.EntityType).AI_MODEL_SETTINGS" + condition = "#event.entityId.getEntityType() != T(org.thingsboard.server.common.data.EntityType).AI_MODEL" ) public void handleEvent(DeleteEntityEvent event) { executorService.submit(() -> { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelService.java similarity index 54% rename from application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java rename to application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelService.java index 69f130461c..264b82dd33 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java +++ b/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; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelSettingsService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelService.java similarity index 76% rename from application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelSettingsService.java rename to application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelService.java index 0d66c171a7..0b09423ffa 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelSettingsService.java +++ b/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); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index ba7b810c7d..701fed952b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -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 entityTypes; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index 6b2f6f8ccf..7a824ca735 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -18,8 +18,8 @@ package org.thingsboard.server.service.security.permission; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.ai.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 aiModelSettingsPermissionChecker = new PermissionChecker<>() { + private static final PermissionChecker 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()); } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index 82f855b7c2..b000041a26 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/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 diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelSettingsExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelExportService.java similarity index 76% rename from application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelSettingsExportService.java rename to application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelExportService.java index a5ba16e9b1..8d6097b726 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelSettingsExportService.java +++ b/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> { +class AiModelExportService extends BaseEntityExportService> { @Override public Set getSupportedEntityTypes() { - return Set.of(EntityType.AI_MODEL_SETTINGS); + return Set.of(EntityType.AI_MODEL); } } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelSettingsImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelImportService.java similarity index 50% rename from application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelSettingsImportService.java rename to application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelImportService.java index e5666e5003..34e70adb11 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelSettingsImportService.java +++ b/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> { +class AiModelImportService extends BaseEntityImportService> { - private final AiModelSettingsService aiModelSettingsService; + private final AiModelService aiModelService; @Override protected void setOwner( TenantId tenantId, - AiModelSettings settings, - BaseEntityImportService>.IdProvider idProvider + AiModel model, + BaseEntityImportService>.IdProvider idProvider ) { - settings.setTenantId(tenantId); + model.setTenantId(tenantId); } @Override - protected AiModelSettings prepare( + protected AiModel prepare( EntitiesImportCtx ctx, - AiModelSettings settings, - AiModelSettings oldEntity, - EntityExportData exportData, - BaseEntityImportService>.IdProvider idProvider + AiModel model, + AiModel oldModel, + EntityExportData exportData, + BaseEntityImportService>.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 exportData, - BaseEntityImportService>.IdProvider idProvider, + AiModel model, + EntityExportData exportData, + BaseEntityImportService>.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; } } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 429bbd4a29..c4bf6bf3c8 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/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}" diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java similarity index 55% rename from common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java index 09219f238e..3ad12048cf 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsService.java +++ b/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 findAiModelSettingsById(TenantId tenantId, AiModelSettingsId settingsId); + Optional findAiModelById(TenantId tenantId, AiModelId modelId); - PageData findAiModelSettingsByTenantId(TenantId tenantId, PageLink pageLink); + PageData findAiModelsByTenantId(TenantId tenantId, PageLink pageLink); - Optional findAiModelSettingsByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); + Optional findAiModelByTenantIdAndId(TenantId tenantId, AiModelId modelId); - FluentFuture> findAiModelSettingsByTenantIdAndIdAsync(TenantId tenantId, AiModelSettingsId settingsId); + FluentFuture> findAiModelByTenantIdAndIdAsync(TenantId tenantId, AiModelId modelId); - boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); + boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index c5df7c10c0..b55453f393 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -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"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index dddafe05bc..8c132a3eba 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -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"; } }; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java similarity index 67% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java index 63e94f1974..4d7bb21930 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java +++ b/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 implements HasTenantId, HasVersion, ExportableEntity { +public final class AiModel extends BaseData implements HasTenantId, HasVersion, ExportableEntity { @Serial private static final long serialVersionUID = 9017108678716011604L; @@ -47,7 +47,7 @@ public final class AiModelSettings extends BaseData 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 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 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 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()); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatRequest.java index aa737b27d3..7e43520b79 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatRequest.java +++ b/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() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java deleted file mode 100644 index affa159e06..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java +++ /dev/null @@ -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 { - - 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 withModelConfig(C config); - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java index 1b0ab7921f..d9e6a1753e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java @@ -15,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(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java deleted file mode 100644 index b9a2737b20..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java +++ /dev/null @@ -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> extends AiModel - 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 withModelConfig(C config); - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java index 67d9827cc6..2284d8b485 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java @@ -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> 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(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java similarity index 75% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java index 356bdd5c42..1f4e4645dd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java +++ b/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 { + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { @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 {} - @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java similarity index 74% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java index 9ab43799fb..69af0f8c2b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java +++ b/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 { + @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 { @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 {} - @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java similarity index 72% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java index f2cb47190b..afe5f6c563 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java +++ b/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 { + @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 { @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 {} - @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java similarity index 72% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java index 723f69299d..ecadb7d453 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java +++ b/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 { + @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 { @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 {} - @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java similarity index 71% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java index b9cb65d3d9..1922dea6a1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/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 { + @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 { @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 {} - @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java similarity index 71% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java index 83b7506e11..e8691c0926 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java +++ b/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 { + @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 { @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 {} - @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java index ea9fb80e73..c9c1bc3173 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java @@ -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); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java similarity index 72% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java index 58ed807819..ea2409ba35 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/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 { + @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 { @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 {} - @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java similarity index 73% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java index 4250291c32..95f6f1cdc3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/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 { + @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 { @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 {} - @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java similarity index 72% rename from common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelSettingsId.java rename to common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java index 83b4fefab2..cac9e8200c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelSettingsId.java +++ b/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)); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index 64504545d4..3638ed1535 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -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!"); }; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java index 6914b75636..a89fc0b670 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java +++ b/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 {} diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index f3cb8a5eea..9d213c7fdb 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/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 { diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheEvictEvent.java similarity index 58% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheEvictEvent.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheEvictEvent.java index 3bf7dce9ba..b0d4b6fdb6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheEvictEvent.java +++ b/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); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheKey.java similarity index 68% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheKey.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheKey.java index 60d0ccaee9..6b73ad7b28 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheKey.java +++ b/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; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCaffeineCache.java similarity index 75% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCaffeineCache.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCaffeineCache.java index d758cb21c5..165efcd4e2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCaffeineCache.java +++ b/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 { +class AiModelCaffeineCache extends VersionedCaffeineTbCache { - AiModelSettingsCaffeineCache(CacheManager cacheManager) { - super(cacheManager, CacheConstants.AI_MODEL_SETTINGS_CACHE); + AiModelCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.AI_MODEL_CACHE); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelDao.java similarity index 60% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelDao.java index c8d6b65369..e788685bfa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java +++ b/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, ExportableEntityDao { +public interface AiModelDao extends TenantEntityDao, ExportableEntityDao { - Optional findByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); + Optional findByTenantIdAndId(TenantId tenantId, AiModelId modelId); - boolean deleteById(TenantId tenantId, AiModelSettingsId settingsId); + boolean deleteById(TenantId tenantId, AiModelId modelId); - Set deleteByTenantId(TenantId tenantId); + Set deleteByTenantId(TenantId tenantId); - boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); + boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelRedisCache.java similarity index 71% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsRedisCache.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelRedisCache.java index 8674c522ff..7bec37875f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsRedisCache.java +++ b/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 { +class AiModelRedisCache extends VersionedRedisTbCache { - 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)); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java new file mode 100644 index 0000000000..b091a29247 --- /dev/null +++ b/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 implements AiModelService { + + private final DataValidator 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 findAiModelById(TenantId tenantId, AiModelId modelId) { + return Optional.ofNullable(aiModelDao.findById(tenantId, modelId.getId())); + } + + @Override + public PageData findAiModelsByTenantId(TenantId tenantId, PageLink pageLink) { + validatePageLink(pageLink, AiModelEntity.ALLOWED_SORT_PROPERTIES); + return aiModelDao.findAllByTenantId(tenantId, pageLink); + } + + @Override + public Optional 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> 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> 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 deleted = aiModelDao.deleteByTenantId(tenantId); + deleted.forEach(id -> publishEvictEvent(new AiModelCacheEvictEvent.Deleted(AiModelCacheKey.of(tenantId, id)))); + } + + @Override + public EntityType getEntityType() { + return EntityType.AI_MODEL; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java deleted file mode 100644 index 2494ab6a15..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java +++ /dev/null @@ -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 implements AiModelSettingsService { - - private final DataValidator 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 findAiModelSettingsById(TenantId tenantId, AiModelSettingsId settingsId) { - return Optional.ofNullable(aiModelSettingsDao.findById(tenantId, settingsId.getId())); - } - - @Override - public PageData findAiModelSettingsByTenantId(TenantId tenantId, PageLink pageLink) { - validatePageLink(pageLink, AiModelSettingsEntity.ALLOWED_SORT_PROPERTIES); - return aiModelSettingsDao.findAllByTenantId(tenantId, pageLink); - } - - @Override - public Optional findAiModelSettingsByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) { - var cacheKey = AiModelSettingsCacheKey.of(tenantId, settingsId); - return Optional.ofNullable(cache.get(cacheKey, () -> aiModelSettingsDao.findByTenantIdAndId(tenantId, settingsId).orElse(null))); - } - - @Override - public FluentFuture> 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> 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 deleted = aiModelSettingsDao.deleteByTenantId(tenantId); - deleted.forEach(id -> publishEvictEvent(new AiModelSettingsCacheEvictEvent.Deleted(AiModelSettingsCacheKey.of(tenantId, id)))); - } - - @Override - public EntityType getEntityType() { - return EntityType.AI_MODEL_SETTINGS; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java index 28c96c4675..2f23d20f00 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java @@ -47,7 +47,7 @@ public class CleanUpService { private final Set 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 diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 6245d75214..ca59d3bce0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -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}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelEntity.java similarity index 62% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelEntity.java index 25aa7a0018..d4f3d36db6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java +++ b/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 { +@Table(name = ModelConstants.AI_MODEL_TABLE_NAME) +public class AiModelEntity extends BaseVersionedEntity { public static final Map COLUMN_MAP = Map.of( "createdTime", "created_time", "provider", "(configuration ->> 'provider')", - "modelId", "(configuration -> 'modelConfig' ->> 'modelId')" + "modelId", "(configuration ->> 'modelId')" ); public static final Set 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 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()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java similarity index 51% rename from dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java rename to dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java index fdba1f4e0c..fdccf2955f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java +++ b/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 { +class AiModelDataValidator extends DataValidator { private final TenantService tenantService; - private final AiModelSettingsDao aiModelSettingsDao; + private final AiModelDao aiModelDao; @Override - protected AiModelSettings validateUpdate(TenantId tenantId, AiModelSettings settings) { - Optional existing = aiModelSettingsDao.findByTenantIdAndId(tenantId, settings.getId()); + protected AiModel validateUpdate(TenantId tenantId, AiModel model) { + Optional 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!"); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelRepository.java similarity index 55% rename from dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelRepository.java index 6be7d97f33..0a746f15ab 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java +++ b/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, ExportableEntityRepository { +interface AiModelRepository extends JpaRepository, ExportableEntityRepository { - Optional findByTenantIdAndId(UUID tenantId, UUID id); + Optional findByTenantIdAndId(UUID tenantId, UUID id); - Optional findByTenantIdAndName(UUID tenantId, String name); + Optional 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 findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); + Page 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 findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); - @Query("SELECT externalId FROM AiModelSettingsEntity WHERE id = :id") + @Query("SELECT externalId FROM AiModelEntity WHERE id = :id") Optional 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 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 ids); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelDao.java similarity index 50% rename from dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelDao.java index 38e7280313..e0c145fe03 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java +++ b/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 implements AiModelSettingsDao { +class JpaAiModelDao extends JpaAbstractDao implements AiModelDao { - private final AiModelSettingsRepository aiModelSettingsRepository; + private final AiModelRepository aiModelRepository; @Override - public Optional findByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) { - return aiModelSettingsRepository.findByTenantIdAndId(tenantId.getId(), settingsId.getId()).map(DaoUtil::getData); + public Optional 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 findAllByTenantId(TenantId tenantId, PageLink pageLink) { + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { return findByTenantId(tenantId.getId(), pageLink); } @Override - public PageData findByTenantId(UUID tenantId, PageLink pageLink) { - return DaoUtil.toPageData(aiModelSettingsRepository.findByTenantId( + public PageData findByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData(aiModelRepository.findByTenantId( tenantId, StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), toPageRequest(pageLink)) ); } @Override - public PageData findIdsByTenantId(UUID tenantId, PageLink pageLink) { - return DaoUtil.pageToPageData(aiModelSettingsRepository.findIdsByTenantId(tenantId, toPageRequest(pageLink)).map(AiModelSettingsId::new)); + public PageData 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 0; + public boolean deleteById(TenantId tenantId, AiModelId modelId) { + return aiModelRepository.deleteByIdIn(Set.of(modelId.getId())) > 0; } @Override - public Set deleteByTenantId(TenantId tenantId) { - return aiModelSettingsRepository.deleteByTenantId(tenantId.getId()).stream() - .map(AiModelSettingsId::new) + public Set 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 getEntityClass() { - return AiModelSettingsEntity.class; + protected Class getEntityClass() { + return AiModelEntity.class; } @Override - protected JpaRepository getRepository() { - return aiModelSettingsRepository; + protected JpaRepository getRepository() { + return aiModelRepository; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 9b37e9c2dd..0eb4a60e6d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -183,7 +183,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService> FluentFuture sendChatRequestAsync(AiChatModel chatModel, ChatRequest chatRequest); + > FluentFuture sendChatRequestAsync(AiChatModelConfig chatModelConfig, ChatRequest chatRequest); } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 1eeb644bba..d2687a1b10 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -44,7 +44,7 @@ import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.ai.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 diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 62d92ef2ce..93c2cafaae 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -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 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 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 > FluentFuture 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 chatModel = (AiChatModel) settingsOpt.get().getConfiguration(); + AiChatModelConfig chatModelConfig = (AiChatModelConfig) 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; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index ebfcf943f7..eba3d5fef9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -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 { @NotNull - private AiModelSettingsId aiModelSettingsId; + private AiModelId aiModelId; @Pattern(regexp = ".*\\S.*", message = "must not be blank") @Length(min = 1, max = 10000) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java index 130332ad64..a8ae187c84 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -19,7 +19,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.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()); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index 3713dbea3e..2d7b8a0d30 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -40,7 +40,7 @@ import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.ai.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); From ad7cfb94acdf11950131db08c6a5efe517f8ff38 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 9 Jul 2025 18:07:02 +0300 Subject: [PATCH 166/435] UI: Change entity type name --- ui-ngx/src/app/core/http/ai-model.service.ts | 8 +++---- ui-ngx/src/app/core/http/entity.service.ts | 4 ++-- .../external/ai-config.component.html | 2 +- .../ai-model/ai-model-table-config.resolve.ts | 8 +++---- .../ai-model/ai-model-dialog.component.html | 12 +++++----- .../ai-model/ai-model-dialog.component.ts | 18 +++++++-------- .../check-connectivity-dialog.component.ts | 10 ++++----- .../entity/entity-autocomplete.component.ts | 2 +- .../src/app/shared/models/ai-model.models.ts | 22 +++++++++---------- .../app/shared/models/entity-type.models.ts | 6 ++--- .../src/app/shared/models/id/ai-model-id.ts | 2 +- ui-ngx/src/app/shared/models/vc.models.ts | 4 ++-- 12 files changed, 46 insertions(+), 52 deletions(-) diff --git a/ui-ngx/src/app/core/http/ai-model.service.ts b/ui-ngx/src/app/core/http/ai-model.service.ts index b2f63ba583..64e003c30e 100644 --- a/ui-ngx/src/app/core/http/ai-model.service.ts +++ b/ui-ngx/src/app/core/http/ai-model.service.ts @@ -32,19 +32,19 @@ export class AiModelService { ) {} public saveAiModel(aiModel: AiModel, config?: RequestConfig): Observable { - return this.http.post('/api/ai/model/settings', aiModel, defaultHttpOptionsFromConfig(config)); + return this.http.post('/api/ai/model', aiModel, defaultHttpOptionsFromConfig(config)); } public getAiModels(pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/ai/model/settings${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + return this.http.get>(`/api/ai/model${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } public getAiModelById(aiModelId: string, config?: RequestConfig): Observable { - return this.http.get(`/api/ai/model/settings/${aiModelId}`, defaultHttpOptionsFromConfig(config)); + return this.http.get(`/api/ai/model/${aiModelId}`, defaultHttpOptionsFromConfig(config)); } public deleteAiModel(aiModelId: string, config?: RequestConfig) { - return this.http.delete(`/api/ai/model/settings/${aiModelId}`, defaultHttpOptionsFromConfig(config)); + return this.http.delete(`/api/ai/model/${aiModelId}`, defaultHttpOptionsFromConfig(config)); } public checkConnectivity(aiModelWithUserMsg: AiModelWithUserMsg, config?: RequestConfig): Observable { diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 17a755ad30..ac6e4b7a7a 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -185,7 +185,7 @@ export class EntityService { case EntityType.MOBILE_APP_BUNDLE: observable = this.mobileAppService.getMobileAppBundleInfoById(entityId, config); break; - case EntityType.AI_MODEL_SETTINGS: + case EntityType.AI_MODEL: observable = this.aiModelService.getAiModelById(entityId, config); break; } @@ -490,7 +490,7 @@ export class EntityService { pageLink.sortOrder.property = 'title'; entitiesObservable = this.mobileAppService.getTenantMobileAppBundleInfos(pageLink, config); break; - case EntityType.AI_MODEL_SETTINGS: + case EntityType.AI_MODEL: pageLink.sortOrder.property = 'name'; entitiesObservable = this.aiModelService.getAiModels(pageLink, config); break; diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html index f085913d5c..8846858a8d 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html @@ -28,7 +28,7 @@ appearance="outline" labelText="ai-models.ai-model" (entityChanged)="onEntityChange($event)" - [entityType]="entityType.AI_MODEL_SETTINGS" + [entityType]="entityType.AI_MODEL" (createNew)="createModelAi('modelSettingsId')" formControlName="modelSettingsId"> diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts index 9f135d002c..6e9bde0b60 100644 --- a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts @@ -46,11 +46,11 @@ export class AiModelsTableConfigResolver { private dialog: MatDialog ) { this.config.selectionEnabled = true; - this.config.entityType = EntityType.AI_MODEL_SETTINGS; + this.config.entityType = EntityType.AI_MODEL; this.config.addAsTextButton = true; this.config.detailsPanelEnabled = false; - this.config.entityTranslations = entityTypeTranslations.get(EntityType.AI_MODEL_SETTINGS); - this.config.entityResources = entityTypeResources.get(EntityType.AI_MODEL_SETTINGS); + this.config.entityTranslations = entityTypeTranslations.get(EntityType.AI_MODEL); + this.config.entityResources = entityTypeResources.get(EntityType.AI_MODEL); this.config.headerComponent = AiModelTableHeaderComponent; this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'}; @@ -65,7 +65,7 @@ export class AiModelsTableConfigResolver { entity => this.translate.instant(AiProviderTranslations.get(entity.configuration.provider)) ), new EntityTableColumn('aiModel', 'ai-models.ai-model', '33%', - entity => entity.configuration.modelConfig.modelId, () => ({}), false + entity => entity.configuration.modelId, () => ({}), false ) ) diff --git a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html index 1a8fae3b45..97eeb79f02 100644 --- a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html @@ -117,7 +117,7 @@
-
+
ai-models.configuration
@@ -139,7 +139,7 @@ matTooltipPosition="above" matTooltipClass="tb-error-tooltip" [matTooltip]="'ai-models.temperature-min' | translate" - *ngIf="aiModelForms.get('configuration').get('modelConfig').get('temperature').hasError('min')" + *ngIf="aiModelForms.get('configuration').get('temperature').hasError('min')" class="tb-error"> warning @@ -156,8 +156,8 @@ matTooltipPosition="above" matTooltipClass="tb-error-tooltip" [matTooltip]="'ai-models.top-p-min-max' | translate" - *ngIf="aiModelForms.get('configuration').get('modelConfig').get('topP').hasError('min') || - aiModelForms.get('configuration').get('modelConfig').get('topP').hasError('max')" + *ngIf="aiModelForms.get('configuration').get('topP').hasError('min') || + aiModelForms.get('configuration').get('topP').hasError('max')" class="tb-error"> warning @@ -176,7 +176,7 @@ matTooltipPosition="above" matTooltipClass="tb-error-tooltip" [matTooltip]="'ai-models.top-k-min' | translate" - *ngIf="aiModelForms.get('configuration').get('modelConfig').get('topK').hasError('min')" + *ngIf="aiModelForms.get('configuration').get('topK').hasError('min')" class="tb-error"> warning @@ -216,7 +216,7 @@ matTooltipPosition="above" matTooltipClass="tb-error-tooltip" [matTooltip]="'ai-models.max-output-token-min' | translate" - *ngIf="aiModelForms.get('configuration').get('modelConfig').get('maxOutputTokens').hasError('min')" + *ngIf="aiModelForms.get('configuration').get('maxOutputTokens').hasError('min')" class="tb-error"> warning diff --git a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts index 5dd63d3f5b..04087dcb79 100644 --- a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts @@ -91,15 +91,13 @@ export class AIModelDialogComponent extends DialogComponent { this.provider = provider; - this.aiModelForms.get('configuration.modelConfig').reset({}); + // this.aiModelForms.get('configuration').reset({}); this.aiModelForms.get('configuration.providerConfig').reset({}); this.updateValidation(provider); }) diff --git a/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.ts b/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.ts index 70896cdc6d..e7f0d8c174 100644 --- a/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.ts @@ -54,15 +54,13 @@ export class CheckConnectivityDialogComponent extends DialogComponent, 'label'>, HasTenantId serviceAccountKey?: string; serviceAccountKeyFileName?: string }; - modelConfig: { - modelId: string; - temperature?: number | null; - topP?: number; - topK?: number; - frequencyPenalty?: number; - presencePenalty?: number; - maxOutputTokens?: number; - } + modelId: string; + temperature?: number | null; + topP?: number; + topK?: number; + frequencyPenalty?: number; + presencePenalty?: number; + maxOutputTokens?: number; } } @@ -131,7 +129,7 @@ export interface AiModelWithUserMsg { userMessage: { contents: Array<{contentType: string; text: string}>; } - chatModel: { + chatModelConfig: { modelType: string; provider: AiProvider providerConfig: { @@ -144,11 +142,11 @@ export interface AiModelWithUserMsg { serviceAccountKey?: string; serviceAccountKeyFileName?: string }; - modelConfig: { + // chatModelConfig: { modelId: string; maxRetries: number; timeoutSeconds: number; - } + // } } } diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 3b9e5f5427..6e7ba24578 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -51,7 +51,7 @@ export enum EntityType { MOBILE_APP_BUNDLE = 'MOBILE_APP_BUNDLE', MOBILE_APP = 'MOBILE_APP', CALCULATED_FIELD = 'CALCULATED_FIELD', - AI_MODEL_SETTINGS = 'AI_MODEL_SETTINGS', + AI_MODEL = 'AI_MODEL', } export enum AliasEntityType { @@ -496,7 +496,7 @@ export const entityTypeTranslations = new Map = [ EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, - EntityType.AI_MODEL_SETTINGS, + EntityType.AI_MODEL, ]; export const entityTypesWithoutRelatedData = new Set([ @@ -46,7 +46,7 @@ export const entityTypesWithoutRelatedData = new Set Date: Wed, 9 Jul 2025 20:05:06 +0300 Subject: [PATCH 167/435] UI: Refactoring --- ui-ngx/src/app/modules/common/modules-map.ts | 2 - .../check-connectivity-dialog.component.html | 0 .../check-connectivity-dialog.component.scss | 0 .../check-connectivity-dialog.component.ts | 4 +- .../home/components/home-components.module.ts | 3 + .../common/time-unit-input.component.html | 98 +++------ .../common/time-unit-input.component.ts | 29 ++- .../external/ai-config.component.html | 28 +-- .../rule-node/external/ai-config.component.ts | 36 ++-- .../ai-model/ai-model-table-config.resolve.ts | 11 +- .../ai-model-table-header.component.scss | 18 -- .../ai-model-table-header.component.ts | 2 +- .../ai-model/ai-model-dialog.component.html | 166 ++++++++------- .../ai-model/ai-model-dialog.component.ts | 54 ++--- .../models-list-autocomplete.component.ts | 4 +- .../src/app/shared/models/ai-model.models.ts | 201 ++++++++++++------ ui-ngx/src/app/shared/shared.module.ts | 3 - 17 files changed, 335 insertions(+), 324 deletions(-) rename ui-ngx/src/app/{shared => modules/home}/components/ai-model/check-connectivity-dialog.component.html (100%) rename ui-ngx/src/app/{shared => modules/home}/components/ai-model/check-connectivity-dialog.component.scss (100%) rename ui-ngx/src/app/{shared => modules/home}/components/ai-model/check-connectivity-dialog.component.ts (95%) delete mode 100644 ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.scss diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 997af07462..60a26e4211 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -337,7 +337,6 @@ import * as AggregationTypeSelectComponent from '@shared/components/time/aggrega import * as AggregationOptionsConfigComponent from '@shared/components/time/aggregation/aggregation-options-config-panel.component'; import * as IntervalOptionsConfigPanelComponent from '@shared/components/time/interval-options-config-panel.component'; import * as AIModelDialogComponent from '@shared/components/ai-model/ai-model-dialog.component'; -import * as CheckConnectivityDialogComponent from '@shared/components/ai-model/check-connectivity-dialog.component'; import * as ModelsListAutocompleteComponent from '@shared/components/ai-model/models-list-autocomplete.component'; import { IModulesMap } from '@modules/common/modules-map.models'; @@ -536,7 +535,6 @@ class ModulesMap implements IModulesMap { '@shared/components/image/multiple-gallery-image-input.component': MultipleGalleryImageInputComponent, '@shared/components/popover.service': TbPopoverService, '@shared/components/ai-model/ai-model-dialog.component': AIModelDialogComponent, - '@shared/components/ai-model/check-connectivity-dialog.component': CheckConnectivityDialogComponent, '@shared/components/ai-model/models-list-autocomplete.component': ModelsListAutocompleteComponent, diff --git a/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html similarity index 100% rename from ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.html rename to ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html diff --git a/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.scss b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.scss similarity index 100% rename from ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.scss rename to ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.scss diff --git a/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts similarity index 95% rename from ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.ts rename to ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts index e7f0d8c174..3b6c96d426 100644 --- a/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts @@ -20,7 +20,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { AiModel, AiModelWithUserMsg } from '@shared/models/ai-model.models'; +import { AiModel, AiModelWithUserMsg, ModelType } from '@shared/models/ai-model.models'; import { AiModelService } from '@core/http/ai-model.service'; export interface AIModelDialogData { @@ -55,7 +55,7 @@ export class CheckConnectivityDialogComponent extends DialogComponent
- @if (outlined) { -
-
{{ labelText }}
-
- - - - warning - - - warning - - - warning - - - - - @for (timeUnit of timeUnits; track timeUnit) { - {{ timeUnitTranslations.get(timeUnit) | translate }} - } - - -
+ + @if (labelText && !inlineField) { + {{ labelText }} + } + +
+
- } @else { - - {{ labelText }} - -
- -
+ @if (inlineField) { + + warning + + } @else { - - {{ requiredText }} + + {{ hasError }} - - {{ minErrorText }} - - - {{ maxErrorText }} - -
- + } + + + @if (!inlineField) { rule-node-config.units - - @for (timeUnit of timeUnits; track timeUnit) { - {{ timeUnitTranslations.get(timeUnit) | translate }} - } - - - } + } + + @for (timeUnit of timeUnits; track timeUnit) { + {{ timeUnitTranslations.get(timeUnit) | translate }} + } + +
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts index 2cd8e7d879..e31d9abf9e 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts @@ -30,7 +30,7 @@ import { isDefinedAndNotNull, isNumeric } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { coerceBoolean, coerceNumber } from '@shared/decorators/coercion'; import { DAY, HOUR, MINUTE, SECOND } from '@shared/models/time/time.models'; -import { SubscriptSizing } from '@angular/material/form-field'; +import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; interface TimeUnitInputModel { time: number; @@ -55,9 +55,6 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, @Input() labelText: string; - @Input() - labelIconHintTooltipText: string; - @Input() @coerceBoolean() required: boolean; @@ -83,12 +80,11 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, subscriptSizing: SubscriptSizing = 'fixed'; @Input() - @coerceBoolean() - filterTimeUnitsByMaxTime = false; + appearance: MatFormFieldAppearance = 'fill'; @Input() @coerceBoolean() - outlined = false; + inlineField: boolean; timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[]; @@ -115,12 +111,13 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, } ngOnInit() { - if (this.filterTimeUnitsByMaxTime && this.maxTime) { - if (this.maxTime < 60) { + if (this.maxTime) { + const maxTimeMs = this.maxTime * SECOND; + if (maxTimeMs < MINUTE) { this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.MINUTES && item !== TimeUnit.HOURS && item !== TimeUnit.DAYS); - } else if (this.maxTime < 3600) { + } else if (maxTimeMs < HOUR) { this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.HOURS && item !== TimeUnit.DAYS); - } else if (this.maxTime < 86400) { + } else if (maxTimeMs < DAY) { this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.DAYS); } } @@ -157,6 +154,16 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, }); } + get hasError(): string { + if (this.timeInputForm.get('time').hasError('required') && this.requiredText) { + return this.requiredText; + } else if (this.timeInputForm.get('time').hasError('min') && this.minErrorText) { + return this.minErrorText; + } else if (this.timeInputForm.get('time').hasError('max') && this.maxErrorText) { + return this.maxErrorText; + } + } + registerOnChange(fn: any) { this.propagateChange = fn; } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html index 8846858a8d..ef9dbe04a4 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html @@ -96,19 +96,21 @@ rule-node-config.ai.advanced-settings
- - +
+
{{ 'rule-node-config.ai.timeout' | translate }}
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts index 283abb636c..3b7f0a92d6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts @@ -20,15 +20,9 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/m import { EntityType } from '@shared/models/entity-type.models'; import { MatDialog } from '@angular/material/dialog'; import { AIModelDialogComponent, AIModelDialogData } from '@shared/components/ai-model/ai-model-dialog.component'; -import { AiModel, AiProvider } from '@shared/models/ai-model.models'; +import { AiModel, AiRuleNodeResponseFormatTypeOnlyText, ResponseFormat } from '@shared/models/ai-model.models'; import { deepTrim } from '@core/utils'; -enum ResponseFormat { - TEXT = 'TEXT', - JSON = 'JSON', - JSON_SCHEMA = 'JSON_SCHEMA' -} - @Component({ selector: 'tb-external-node-ai-config', templateUrl: './ai-config.component.html', @@ -53,14 +47,14 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { protected onConfigurationSet(configuration: RuleNodeConfiguration) { this.aiConfigForm = this.fb.group({ - modelSettingsId: [configuration ? configuration.modelSettingsId : null, [Validators.required]], - systemPrompt: [configuration ? configuration.systemPrompt : '', [Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], - userPrompt: [configuration ? configuration.userPrompt : '', [Validators.required, Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], + modelSettingsId: [configuration?.modelSettingsId ?? null, [Validators.required]], + systemPrompt: [configuration?.systemPrompt ?? '', [Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], + userPrompt: [configuration?.userPrompt ?? '', [Validators.required, Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], responseFormat: this.fb.group({ - type: [configuration ? configuration.responseFormat.type : ResponseFormat.JSON, []], - schema: [configuration ? configuration.responseFormat.schema : null, []], + type: [configuration?.responseFormat?.type ?? ResponseFormat.JSON, []], + schema: [configuration?.responseFormat?.schema ?? null, [Validators.required]], }), - timeoutSeconds: [configuration ? configuration.timeoutSeconds : 60, []] + timeoutSeconds: [configuration?.timeoutSeconds ?? 60, []] }); } @@ -69,15 +63,11 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { } protected updateValidators(emitEvent: boolean) { - const responseFormatType = this.aiConfigForm.get('responseFormat.type').value; - if (responseFormatType === ResponseFormat.JSON_SCHEMA) { - this.aiConfigForm.get('responseFormat.schema').setValidators([Validators.required]); - this.aiConfigForm.get('responseFormat.schema').enable(); + if (this.aiConfigForm.get('responseFormat.type').value === ResponseFormat.JSON_SCHEMA) { + this.aiConfigForm.get('responseFormat.schema').enable({emitEvent: false}); } else { - this.aiConfigForm.get('responseFormat.schema').setValidators([]); - this.aiConfigForm.get('responseFormat.schema').disable(); + this.aiConfigForm.get('responseFormat.schema').disable({emitEvent: false}); } - this.aiConfigForm.get('responseFormat.schema').updateValueAndValidity({emitEvent}); } protected prepareOutputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration { @@ -86,9 +76,7 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { onEntityChange($event: AiModel) { if ($event) { - if ($event.configuration.provider === AiProvider.AMAZON_BEDROCK || - $event.configuration.provider === AiProvider.ANTHROPIC || - $event.configuration.provider === AiProvider.GITHUB_MODELS) { + if (AiRuleNodeResponseFormatTypeOnlyText.includes($event.configuration.provider)) { this.aiConfigForm.get('responseFormat.type').patchValue(ResponseFormat.TEXT, {emitEvent: false}); this.aiConfigForm.get('responseFormat.type').disable({emitEvent: false}); } @@ -97,7 +85,7 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { } } - createModelAi(formControl: string,) { + createModelAi(formControl: string) { this.dialog.open(AIModelDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts index 6e9bde0b60..3f3ded2abb 100644 --- a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts @@ -79,6 +79,11 @@ export class AiModelsTableConfigResolver { this.config.entitiesFetchFunction = pageLink => this.aiModelService.getAiModels(pageLink); this.config.cellActionDescriptors = this.configureCellActions(); + + this.config.handleRowClick = ($event, model) => { + this.editModel(model); + return true; + }; } resolve(_route: ActivatedRouteSnapshot): EntityTableConfig { @@ -97,11 +102,7 @@ export class AiModelsTableConfigResolver { } private editModel(AIModel: AiModel): void { - this.addModel(AIModel, false).subscribe((res) => { - if (res) { - this.config.updateData(); - } - }); + this.addModel(AIModel, false).subscribe(); } private addModel(AIModel: AiModel, isAdd = false): Observable { diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.scss deleted file mode 100644 index acb9e2dc87..0000000000 --- a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.scss +++ /dev/null @@ -1,18 +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. - */ -:host{ - width: 100%; -} diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts index 4fbc906b80..48889dc877 100644 --- a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts @@ -23,7 +23,7 @@ import { AiModel } from '@shared/models/ai-model.models'; @Component({ selector: 'tb-ai-model-table-header', templateUrl: './ai-model-table-header.component.html', - styleUrls: ['./ai-model-table-header.component.scss'] + styleUrls: [] }) export class AiModelTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html index 97eeb79f02..2a5a34a2c9 100644 --- a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html @@ -56,7 +56,7 @@
- @if (provider === aiProvider.GITHUB_MODELS) { + @if (AiModelMap.get(provider).providerFieldsList.includes('personalAccessToken')) { ai-models.personal-access-token @@ -65,24 +65,29 @@ {{ 'ai-models.personal-access-token-required' | translate }} - } @else if (provider === aiProvider.GOOGLE_VERTEX_AI_GEMINI) { - + } + @if (AiModelMap.get(provider).providerFieldsList.includes('projectId')) { + ai-models.project-id {{ 'ai-models.project-id-required' | translate }} - + } + @if (AiModelMap.get(provider).providerFieldsList.includes('location')) { + ai-models.location {{ 'ai-models.location-required' | translate }} + } + @if (AiModelMap.get(provider).providerFieldsList.includes('serviceAccountKey')) { - } @else { - @if (provider === aiProvider.AZURE_OPENAI) { - - ai-models.endpoint - - - {{ 'ai-models.endpoint-required' | translate }} - - - - ai-models.service-version - - - } + } + @if (AiModelMap.get(provider).providerFieldsList.includes('endpoint')) { + + ai-models.endpoint + + + {{ 'ai-models.endpoint-required' | translate }} + + + } + @if (AiModelMap.get(provider).providerFieldsList.includes('serviceVersion')) { + + ai-models.service-version + + + } + @if (AiModelMap.get(provider).providerFieldsList.includes('apiKey')) { ai-models.api-key @@ -128,43 +136,46 @@ [errorText]="(provider === aiProvider.AZURE_OPENAI ? 'ai-models.deployment-name-required': 'ai-models.model-id-required') | translate"> -
-
- {{ 'ai-models.temperature' | translate }} -
- - - - warning - - -
-
-
- {{ 'ai-models.top-p' | translate }} + @if (AiModelMap.get(provider).modelFieldsList.includes('temperature')) { +
+
+ {{ 'ai-models.temperature' | translate }} +
+ + + + warning + +
- - - +
+ {{ 'ai-models.top-p' | translate }} +
+ + + - warning - - -
- - @if (provider === aiProvider.GOOGLE_AI_GEMINI || provider === aiProvider.GOOGLE_VERTEX_AI_GEMINI || provider === aiProvider.ANTHROPIC) { + class="tb-error"> + warning + + +
+ } + @if (AiModelMap.get(provider).modelFieldsList.includes('topK')) {
{{ 'ai-models.top-k' | translate }} @@ -183,45 +194,48 @@
} - - @if (provider !== aiProvider.ANTHROPIC && provider !== aiProvider.AMAZON_BEDROCK) { + @if (AiModelMap.get(provider).modelFieldsList.includes('presencePenalty')) {
{{ 'ai-models.presence-penalty' | translate }}
-
+ } + + @if (AiModelMap.get(provider).modelFieldsList.includes('frequencyPenalty')) {
{{ 'ai-models.frequency-penalty' | translate }}
-
} - -
-
- {{ 'ai-models.max-output-token' | translate }} + @if (AiModelMap.get(provider).modelFieldsList.includes('maxOutputTokens')) { +
+
+ {{ 'ai-models.max-output-token' | translate }} +
+ + + + warning + +
- - - - warning - - -
+ }
diff --git a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts index 04087dcb79..d9ae3eaa69 100644 --- a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts @@ -25,9 +25,16 @@ import { StepperOrientation } from '@angular/cdk/stepper'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { EntityType } from '@shared/models/entity-type.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { AiModel, AiProvider, AiProviderTranslations, AiProviderWithApiKey } from '@shared/models/ai-model.models'; +import { + AiModel, + AiModelMap, + AiProvider, + AiProviderTranslations, + ModelType, + ProviderFieldsAllList +} from '@shared/models/ai-model.models'; import { AiModelService } from '@core/http/ai-model.service'; -import { CheckConnectivityDialogComponent } from '@shared/components/ai-model/check-connectivity-dialog.component'; +import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component'; export interface AIModelDialogData { AIModel?: AiModel; @@ -53,7 +60,7 @@ export class AIModelDialogComponent extends DialogComponent { this.provider = provider; - // this.aiModelForms.get('configuration').reset({}); + this.aiModelForms.get('configuration.modelId').reset({}); this.aiModelForms.get('configuration.providerConfig').reset({}); this.updateValidation(provider); }) @@ -114,35 +121,10 @@ export class AIModelDialogComponent extends DialogComponent + this.aiModelForms.get('configuration.providerConfig') + .get(key)[AiModelMap.get(provider).providerFieldsList.includes(key) ? 'enable' : 'disable']() + ) } cancel(): void { diff --git a/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts b/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts index a5cbdb332d..3e3c0cbb66 100644 --- a/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts @@ -99,7 +99,7 @@ export class ModelsListAutocompleteComponent implements ControlValueAccessor, On tap(value => this.updateView(value)), map(value => { const search = value ? value.toLowerCase() : ''; - const options = this.provider ? AiModelMap.get(this.provider) || [] : []; + const options = this.provider ? AiModelMap.get(this.provider).modelList || [] : []; return search ? options.filter(option => option.toLowerCase().includes(search)) : options; }) ); @@ -150,7 +150,7 @@ export class ModelsListAutocompleteComponent implements ControlValueAccessor, On } clear() { - this.selectionFormControl.patchValue(null, {emitEvent: false}); + this.selectionFormControl.patchValue(null, {emitEvent: true}); this.propagateChange(null); this.modelValue = null; setTimeout(() => { diff --git a/ui-ngx/src/app/shared/models/ai-model.models.ts b/ui-ngx/src/app/shared/models/ai-model.models.ts index cd844f57e6..1eb69d867e 100644 --- a/ui-ngx/src/app/shared/models/ai-model.models.ts +++ b/ui-ngx/src/app/shared/models/ai-model.models.ts @@ -19,7 +19,7 @@ import { HasTenantId } from '@shared/models/entity.models'; import { AiModelId } from '@shared/models/id/ai-model-id'; export interface AiModel extends Omit, 'label'>, HasTenantId, ExportableEntity { - modelType: string; + modelType: ModelType; configuration: { provider: AiProvider providerConfig: { @@ -30,10 +30,10 @@ export interface AiModel extends Omit, 'label'>, HasTenantId projectId?: string; location?: string; serviceAccountKey?: string; - serviceAccountKeyFileName?: string + fileName?: string }; modelId: string; - temperature?: number | null; + temperature?: number; topP?: number; topK?: number; frequencyPenalty?: number; @@ -42,6 +42,10 @@ export interface AiModel extends Omit, 'label'>, HasTenantId } } +export enum ModelType { + CHAT = 'CHAT' +} + export enum AiProvider { OPENAI = 'OPENAI', AZURE_OPENAI = 'AZURE_OPENAI', @@ -53,15 +57,6 @@ export enum AiProvider { GITHUB_MODELS = 'GITHUB_MODELS' } -export const AiProviderWithApiKey: AiProvider[] = [ - AiProvider.OPENAI, - AiProvider.AZURE_OPENAI, - AiProvider.GOOGLE_AI_GEMINI, - AiProvider.MISTRAL_AI, - AiProvider.ANTHROPIC, - AiProvider.AMAZON_BEDROCK -] - export const AiProviderTranslations = new Map( [ [AiProvider.OPENAI , 'ai-models.ai-providers.openai'], @@ -75,55 +70,130 @@ export const AiProviderTranslations = new Map( ] ); -export const AiModelMap = new Map( +export const ProviderFieldsAllList = [ + 'apiKey', + 'personalAccessToken', + 'projectId', + 'location', + 'serviceAccountKey', + 'fileName', + 'endpoint', + 'serviceVersion' +]; + +export const ModelFieldsAllList = ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens']; + +export const AiModelMap = new Map([ [ - [AiProvider.OPENAI , [ - 'o4-mini', - 'o3-pro', - 'o3', - 'o3-mini', - 'o1', - 'gpt-4.1', - 'gpt-4.1-mini', - 'gpt-4.1-nano', - 'gpt-4o', - 'gpt-4o-mini' - ]], - [AiProvider.AZURE_OPENAI , []], - [AiProvider.GOOGLE_AI_GEMINI , [ - 'gemini-2.5-pro', - 'gemini-2.5-flash', - 'gemini-2.0-flash', - 'gemini-2.0-flash-lite', - ]], - [AiProvider.GOOGLE_VERTEX_AI_GEMINI , [ - 'gemini-2.5-pro', - 'gemini-2.5-flash', - 'gemini-2.0-flash', - 'gemini-2.0-flash-lite', - ]], - [AiProvider.MISTRAL_AI , [ - 'magistral-medium-latest', - 'magistral-small-latest', - 'mistral-large-latest', - 'mistral-medium-latest', - 'mistral-small-latest', - 'pixtral-large-latest', - 'ministral-8b-latest', - 'ministral-3b-latest', - 'open-mistral-nemo' - ]], - [AiProvider.ANTHROPIC , [ - 'claude-opus-4-0', - 'claude-sonnet-4-0', - 'claude-3-7-sonnet-latest', - 'claude-3-5-sonnet-latest', - 'claude-3-5-haiku-latest' - ]], - [AiProvider.AMAZON_BEDROCK , []], - [AiProvider.GITHUB_MODELS , []] - ] -); + AiProvider.OPENAI, + { + modelList: [ + 'o4-mini', + 'o3-pro', + 'o3', + 'o3-mini', + 'o1', + 'gpt-4.1', + 'gpt-4.1-mini', + 'gpt-4.1-nano', + 'gpt-4o', + 'gpt-4o-mini', + ], + providerFieldsList: ['apiKey'], + modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], + [ + AiProvider.AZURE_OPENAI, + { + modelList: [], + providerFieldsList: ['apiKey', 'endpoint', 'serviceVersion'], + modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], + [ + AiProvider.GOOGLE_AI_GEMINI, + { + modelList: [ + 'gemini-2.5-pro', + 'gemini-2.5-flash', + 'gemini-2.0-flash', + 'gemini-2.0-flash-lite', + ], + providerFieldsList: ['apiKey'], + modelFieldsList: ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], + [ + AiProvider.GOOGLE_VERTEX_AI_GEMINI, + { + modelList: [ + 'gemini-2.5-pro', + 'gemini-2.5-flash', + 'gemini-2.0-flash', + 'gemini-2.0-flash-lite', + ], + providerFieldsList: ['projectId', 'location', 'serviceAccountKey', 'fileName'], + modelFieldsList: ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], + [ + AiProvider.MISTRAL_AI, + { + modelList: [ + 'magistral-medium-latest', + 'magistral-small-latest', + 'mistral-large-latest', + 'mistral-medium-latest', + 'mistral-small-latest', + 'pixtral-large-latest', + 'ministral-8b-latest', + 'ministral-3b-latest', + 'open-mistral-nemo', + ], + providerFieldsList: ['apiKey'], + modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], + [ + AiProvider.ANTHROPIC, + { + modelList: [ + 'claude-opus-4-0', + 'claude-sonnet-4-0', + 'claude-3-7-sonnet-latest', + 'claude-3-5-sonnet-latest', + 'claude-3-5-haiku-latest', + ], + providerFieldsList: ['apiKey'], + modelFieldsList: ['temperature', 'topP', 'topK', 'maxOutputTokens'], + }, + ], + [ + AiProvider.AMAZON_BEDROCK, + { + modelList: [], + providerFieldsList: ['apiKey'], + modelFieldsList: ['temperature', 'topP', 'maxOutputTokens'], + }, + ], + [ + AiProvider.GITHUB_MODELS, + { + modelList: [], + providerFieldsList: ['personalAccessToken'], + modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], +]); + +export const AiRuleNodeResponseFormatTypeOnlyText: AiProvider[] = [AiProvider.AMAZON_BEDROCK, AiProvider.ANTHROPIC, AiProvider.GITHUB_MODELS]; + +export enum ResponseFormat { + TEXT = 'TEXT', + JSON = 'JSON', + JSON_SCHEMA = 'JSON_SCHEMA' +} export interface AiModelWithUserMsg { userMessage: { @@ -140,17 +210,14 @@ export interface AiModelWithUserMsg { projectId?: string; location?: string; serviceAccountKey?: string; - serviceAccountKeyFileName?: string + fileName?: string }; - // chatModelConfig: { - modelId: string; - maxRetries: number; - timeoutSeconds: number; - // } + modelId: string; + maxRetries: number; + timeoutSeconds: number; } } - export interface CheckConnectivityResult { status: string; errorDetails: string; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 8f3de3c26a..0a559361c8 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -230,7 +230,6 @@ import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { MqttVersionSelectComponent } from '@shared/components/mqtt-version-select.component'; import { AIModelDialogComponent } from '@shared/components/ai-model/ai-model-dialog.component'; import { ModelsListAutocompleteComponent } from '@shared/components/ai-model/models-list-autocomplete.component'; -import { CheckConnectivityDialogComponent } from '@shared/components/ai-model/check-connectivity-dialog.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -448,7 +447,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) MqttVersionSelectComponent, AIModelDialogComponent, ModelsListAutocompleteComponent, - CheckConnectivityDialogComponent, ], imports: [ CommonModule, @@ -715,7 +713,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) MqttVersionSelectComponent, AIModelDialogComponent, ModelsListAutocompleteComponent, - CheckConnectivityDialogComponent, ] }) export class SharedModule { } From e7c971a7bbb82d4d62d7db914e805a45beb03e11 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 9 Jul 2025 20:40:14 +0300 Subject: [PATCH 168/435] AI rule node: add node description and details --- .../thingsboard/rule/engine/ai/TbAiNode.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 93c2cafaae..6c4815308a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -52,9 +52,20 @@ import static org.thingsboard.server.dao.service.ConstraintValidator.validateFie @RuleNode( type = ComponentType.EXTERNAL, - name = "AI", - nodeDescription = "Interact with AI", - nodeDetails = "This node makes requests to AI based on a prompt and a input message and returns a response in a form of output message", + name = "AI request", + nodeDescription = "Sends a request to an AI model using system and user prompts. Supports JSON mode.", + nodeDetails = """ + Interact with large language models (LLMs) by sending dynamic requests from your rule chain. + You can select a specific AI model and define its behavior using a system prompt (optional context or role) and a user prompt (the main task). + Both prompts can be populated with data and metadata from the incoming message using patterns. + For example, the $[*] and ${*} patterns allow you to access the all message body and all metadata, respectively. +

+ After sending the request, the node waits for a response within a configured timeout. + You can specify the desired response format as Text, JSON, or provide a specific JSON Schema to structure the output. + The AI-generated content is forwarded as the body of the outgoing message; the originator, message type, and metadata from the incoming message remain unchanged. +

+ Output connections: Success, Failure. + """, configClazz = TbAiNodeConfiguration.class, ruleChainTypes = RuleChainType.CORE ) From ed47569f07f01ab47b40a5214cd8baf69fe59991 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 9 Jul 2025 20:55:54 +0300 Subject: [PATCH 169/435] AI rule node: improve text search query --- .../thingsboard/server/dao/sql/ai/AiModelRepository.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelRepository.java index 0a746f15ab..cbe681fb58 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelRepository.java @@ -42,8 +42,8 @@ interface AiModelRepository extends JpaRepository, Exportab WHERE model.tenant_id = :tenantId AND (:textSearch IS NULL OR model.name ILIKE '%' || :textSearch || '%' - OR (model.configuration ->> 'provider') ILIKE '%' || :textSearch || '%' - OR (model.configuration ->> 'modelId') ILIKE '%' || :textSearch || '%') + OR REPLACE(model.configuration ->> 'provider', '_', ' ') ILIKE '%' || :textSearch || '%' + OR model.configuration ->> 'modelId' ILIKE '%' || :textSearch || '%') """, countQuery = """ SELECT COUNT(*) @@ -51,7 +51,7 @@ interface AiModelRepository extends JpaRepository, Exportab WHERE model.tenant_id = :tenantId AND (:textSearch IS NULL OR model.name ILIKE '%' || :textSearch || '%' - OR (model.configuration ->> 'provider') ILIKE '%' || :textSearch || '%' + OR REPLACE(model.configuration ->> 'provider', '_', ' ') ILIKE '%' || :textSearch || '%' OR (model.configuration ->> 'modelId') ILIKE '%' || :textSearch || '%') """, nativeQuery = true From ff9d3f25d39911e952492042a53ef47ede288a8d Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 9 Jul 2025 21:01:23 +0300 Subject: [PATCH 170/435] AI rule node: add default sorting --- .../java/org/thingsboard/server/dao/sql/ai/JpaAiModelDao.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelDao.java index e0c145fe03..882c86555b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelDao.java @@ -89,7 +89,7 @@ class JpaAiModelDao extends JpaAbstractDao implements Ai sort = JpaSort.unsafe( Sort.Direction.fromString(sortOrder.getDirection().name()), AiModelEntity.COLUMN_MAP.getOrDefault(sortOrder.getProperty(), sortOrder.getProperty()) - ); + ).and(Sort.by(Sort.Direction.ASC, "id")); } return PageRequest.of(pageLink.getPage(), pageLink.getPageSize(), sort); } From 14d89aa5eae9799e3f548ac9d1c1e315f4ee484f Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 9 Jul 2025 21:08:57 +0300 Subject: [PATCH 171/435] AI rule node: add missing cache specs to test application properties --- dao/src/test/resources/application-test.properties | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index a44303107c..1c2c0c5519 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -111,6 +111,9 @@ cache.specs.mobileSecretKey.maxSize=10000 cache.specs.trendzSettings.timeToLiveInMinutes=1440 cache.specs.trendzSettings.maxSize=10000 +cache.specs.aiModel.timeToLiveInMinutes=1440 +cache.specs.aiModel.maxSize=10000 + redis.connection.host=localhost redis.connection.port=6379 redis.connection.db=0 @@ -158,4 +161,4 @@ queue.core.poll-interval=5 queue.core.partitions=2 queue.rule-engine.poll-interval=5 -spring.jpa.properties.hibernate.dialect=org.thingsboard.server.dao.ThingsboardPostgreSQLDialect \ No newline at end of file +spring.jpa.properties.hibernate.dialect=org.thingsboard.server.dao.ThingsboardPostgreSQLDialect From 9bcd3d8849663e7a18503541b55328fe449a0127 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 10 Jul 2025 10:37:09 +0300 Subject: [PATCH 172/435] AI rule node: fix rule engine message metadata (de)serialization --- .../java/org/thingsboard/server/common/msg/TbMsgMetaData.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java index ef083ebccb..60fb1df6aa 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.msg; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import java.io.Serializable; @@ -64,6 +65,7 @@ public final class TbMsgMetaData implements Serializable { return new TbMsgMetaData(data); } + @JsonIgnore public boolean isEmpty() { return data == null || data.isEmpty(); } From fd7faa5a9e06f1a2a89dabcc7bfac4efd2303cf3 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 10 Jul 2025 11:18:59 +0300 Subject: [PATCH 173/435] AI rule node: fix flaky Azure IoT hub node test --- .../common/util/AzureIotHubUtil.java | 24 +++++++++---------- .../engine/mqtt/azure/TbAzureIotHubNode.java | 12 +++++++++- .../mqtt/azure/TbAzureIotHubNodeTest.java | 10 ++++++-- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/common/util/src/main/java/org/thingsboard/common/util/AzureIotHubUtil.java b/common/util/src/main/java/org/thingsboard/common/util/AzureIotHubUtil.java index 2c214460f6..001513b008 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/AzureIotHubUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/AzureIotHubUtil.java @@ -26,11 +26,13 @@ import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Clock; import java.util.Base64; import java.util.Iterator; @Slf4j public final class AzureIotHubUtil { + private static final String BASE_DIR_PATH = System.getProperty("user.dir"); private static final String APP_DIR = "application"; private static final String SRC_DIR = "src"; @@ -52,41 +54,37 @@ public final class AzureIotHubUtil { } } - private static final long SAS_TOKEN_VALID_SECS = 365 * 24 * 60 * 60; - private static final long ONE_SECOND_IN_MILLISECONDS = 1000; + private static final long SAS_TOKEN_VALID_SECS = 365 * 24 * 60 * 60; // one year private static final String SAS_TOKEN_FORMAT = "SharedAccessSignature sr=%s&sig=%s&se=%s"; private static final String USERNAME_FORMAT = "%s/%s/?api-version=2018-06-30"; - private AzureIotHubUtil() { - } + private AzureIotHubUtil() {} public static String buildUsername(String host, String deviceId) { return String.format(USERNAME_FORMAT, host, deviceId); } - public static String buildSasToken(String host, String sasKey) { + public static String buildSasToken(String host, String sasKey, Clock clock) { try { - final String targetUri = URLEncoder.encode(host.toLowerCase(), "UTF-8"); - final long expiryTime = buildExpiresOn(); + final String targetUri = URLEncoder.encode(host.toLowerCase(), StandardCharsets.UTF_8); + final long expiryTime = buildExpiresOn(clock); String toSign = targetUri + "\n" + expiryTime; byte[] keyBytes = Base64.getDecoder().decode(sasKey.getBytes(StandardCharsets.UTF_8)); SecretKeySpec signingKey = new SecretKeySpec(keyBytes, "HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(signingKey); byte[] rawHmac = mac.doFinal(toSign.getBytes(StandardCharsets.UTF_8)); - String signature = URLEncoder.encode(Base64.getEncoder().encodeToString(rawHmac), "UTF-8"); + String signature = URLEncoder.encode(Base64.getEncoder().encodeToString(rawHmac), StandardCharsets.UTF_8); return String.format(SAS_TOKEN_FORMAT, targetUri, signature, expiryTime); } catch (Exception e) { - throw new RuntimeException("Failed to build SAS token!!!", e); + throw new RuntimeException("Failed to build SAS token!", e); } } - private static long buildExpiresOn() { - long expiresOnDate = System.currentTimeMillis(); - expiresOnDate += SAS_TOKEN_VALID_SECS * ONE_SECOND_IN_MILLISECONDS; - return expiresOnDate / ONE_SECOND_IN_MILLISECONDS; + private static long buildExpiresOn(Clock clock) { + return clock.instant().plusSeconds(SAS_TOKEN_VALID_SECS).getEpochSecond(); } public static String getDefaultCaCert() { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java index 2ea56ce799..26c5b3fa42 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java @@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.mqtt.azure; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.annotations.VisibleForTesting; import io.netty.handler.codec.mqtt.MqttVersion; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.AzureIotHubUtil; @@ -36,6 +37,8 @@ import org.thingsboard.server.common.data.plugin.ComponentClusteringMode; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; +import java.time.Clock; + @Slf4j @RuleNode( type = ComponentType.EXTERNAL, @@ -49,6 +52,8 @@ import org.thingsboard.server.common.data.util.TbPair; ) public class TbAzureIotHubNode extends TbMqttNode { + private Clock clock = Clock.systemUTC(); + @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { super.init(ctx); @@ -73,7 +78,7 @@ public class TbAzureIotHubNode extends TbMqttNode { config.setUsername(AzureIotHubUtil.buildUsername(mqttNodeConfiguration.getHost(), config.getClientId())); ClientCredentials credentials = mqttNodeConfiguration.getCredentials(); if (CredentialsType.SAS == credentials.getType()) { - config.setPassword(AzureIotHubUtil.buildSasToken(mqttNodeConfiguration.getHost(), ((AzureIotHubSasCredentials) credentials).getSasKey())); + config.setPassword(AzureIotHubUtil.buildSasToken(mqttNodeConfiguration.getHost(), ((AzureIotHubSasCredentials) credentials).getSasKey(), clock)); } } @@ -81,6 +86,11 @@ public class TbAzureIotHubNode extends TbMqttNode { return initClient(ctx); } + @VisibleForTesting + void setClock(Clock clock) { + this.clock = clock; + } + @Override public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { boolean hasChanges = false; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNodeTest.java index 433d5d4673..c8c1553fa5 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNodeTest.java @@ -34,6 +34,9 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.credentials.CertPemCredentials; import org.thingsboard.rule.engine.mqtt.TbMqttNodeConfiguration; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -77,7 +80,10 @@ public class TbAzureIotHubNodeTest extends AbstractRuleNodeUpgradeTest { @Test public void verifyPrepareMqttClientConfigMethodWithAzureIotHubSasCredentials() throws Exception { - AzureIotHubSasCredentials credentials = new AzureIotHubSasCredentials(); + var fixedClock = Clock.fixed(Instant.parse("2030-01-01T00:00:00Z"), ZoneOffset.UTC); + azureIotHubNode.setClock(fixedClock); + + var credentials = new AzureIotHubSasCredentials(); credentials.setSasKey("testSasKey"); credentials.setCaCert("test-ca-cert.pem"); azureIotHubNodeConfig.setCredentials(credentials); @@ -89,7 +95,7 @@ public class TbAzureIotHubNodeTest extends AbstractRuleNodeUpgradeTest { azureIotHubNode.prepareMqttClientConfig(mqttClientConfig); assertThat(mqttClientConfig.getUsername()).isEqualTo(AzureIotHubUtil.buildUsername(azureIotHubNodeConfig.getHost(), mqttClientConfig.getClientId())); - assertThat(mqttClientConfig.getPassword()).isEqualTo(AzureIotHubUtil.buildSasToken(azureIotHubNodeConfig.getHost(), credentials.getSasKey())); + assertThat(mqttClientConfig.getPassword()).isEqualTo(AzureIotHubUtil.buildSasToken(azureIotHubNodeConfig.getHost(), credentials.getSasKey(), fixedClock)); } @Test From 82d2c1d93a15504698e69b1d53934511c44122ac Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 10 Jul 2025 11:40:14 +0300 Subject: [PATCH 174/435] AI rule node: get rid of model type as record component --- .../thingsboard/server/common/data/ai/model/AiModelConfig.java | 2 -- .../server/common/data/ai/model/chat/AiChatModelConfig.java | 2 ++ .../common/data/ai/model/chat/AmazonBedrockChatModelConfig.java | 2 -- .../common/data/ai/model/chat/AnthropicChatModelConfig.java | 2 -- .../common/data/ai/model/chat/AzureOpenAiChatModelConfig.java | 2 -- .../common/data/ai/model/chat/GitHubModelsChatModelConfig.java | 2 -- .../data/ai/model/chat/GoogleAiGeminiChatModelConfig.java | 2 -- .../data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java | 2 -- .../common/data/ai/model/chat/MistralAiChatModelConfig.java | 2 -- .../server/common/data/ai/model/chat/OpenAiChatModelConfig.java | 2 -- 10 files changed, 2 insertions(+), 18 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java index d9e6a1753e..0a2b41a91f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java @@ -15,7 +15,6 @@ */ 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; @@ -74,7 +73,6 @@ public interface AiModelConfig { }) AiProviderConfig providerConfig(); - @JsonProperty("modelType") AiModelType modelType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java index 2284d8b485..8df5cc0075 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.ai.model.chat; +import com.fasterxml.jackson.annotation.JsonProperty; import dev.langchain4j.model.chat.ChatModel; import org.thingsboard.server.common.data.ai.model.AiModelConfig; import org.thingsboard.server.common.data.ai.model.AiModelType; @@ -28,6 +29,7 @@ public sealed interface AiChatModelConfig> extend ChatModel configure(Langchain4jChatModelConfigurer configurer); @Override + @JsonProperty(value = "modelType", access = JsonProperty.Access.READ_ONLY) default AiModelType modelType() { return AiModelType.CHAT; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java index 1f4e4645dd..d792829c23 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java @@ -23,12 +23,10 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; import lombok.With; -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 AmazonBedrockChatModelConfig( - AiModelType modelType, @NotNull @Valid AmazonBedrockProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java index 69af0f8c2b..478b0a4208 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java @@ -23,12 +23,10 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; import lombok.With; -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 AnthropicChatModelConfig( - AiModelType modelType, @NotNull @Valid AnthropicProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java index afe5f6c563..88098e36e8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java @@ -23,12 +23,10 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; import lombok.With; -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 AzureOpenAiChatModelConfig( - AiModelType modelType, @NotNull @Valid AzureOpenAiProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java index ecadb7d453..57322c7df5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java @@ -23,12 +23,10 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; import lombok.With; -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 GitHubModelsChatModelConfig( - AiModelType modelType, @NotNull @Valid GitHubModelsProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java index 1922dea6a1..1b59bce3b5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java @@ -23,12 +23,10 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; import lombok.With; -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 GoogleAiGeminiChatModelConfig( - AiModelType modelType, @NotNull @Valid GoogleAiGeminiProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java index e8691c0926..cb23406534 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java @@ -23,12 +23,10 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; import lombok.With; -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 GoogleVertexAiGeminiChatModelConfig( - AiModelType modelType, @NotNull @Valid GoogleVertexAiGeminiProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java index ea2409ba35..da15e1a873 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java @@ -23,12 +23,10 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; import lombok.With; -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 MistralAiChatModelConfig( - AiModelType modelType, @NotNull @Valid MistralAiProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java index 95f6f1cdc3..886c020634 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java @@ -23,12 +23,10 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; import lombok.With; -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 OpenAiChatModelConfig( - AiModelType modelType, @NotNull @Valid OpenAiProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, From 17d7931e5dda6aa377fbc2eb13c4973256c7b5e4 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 10 Jul 2025 12:26:10 +0300 Subject: [PATCH 175/435] AI rule node: add basic save API tests --- .../controller/AiModelControllerTest.java | 192 ++++++++++++++++++ .../ai/model/chat/OpenAiChatModelConfig.java | 2 + 2 files changed, 194 insertions(+) create mode 100644 application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java diff --git a/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java new file mode 100644 index 0000000000..1a6ece29f3 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java @@ -0,0 +1,192 @@ +/** + * 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 org.junit.Test; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.web.servlet.ResultActions; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; +import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.service.entitiy.TbLogEntityActionService; +import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; + +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class AiModelControllerTest extends AbstractControllerTest { + + @SpyBean + private EntitiesVersionControlService versionControlService; + + @SpyBean + private TbLogEntityActionService logEntityActionService; + + /* --- Save API tests --- */ + + @Test + public void saveAiModel_whenUserIsSysAdmin_shouldReturnForbidden() throws Exception { + // GIVEN + loginSysAdmin(); + + AiModel model = constructValidModel(); + + // WHEN + ResultActions result = doPost("/api/ai/model", model); + + // THEN + result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission))); + } + + @Test + public void saveAiModel_whenUserIsCustomerUser_shouldReturnForbidden() throws Exception { + // GIVEN + loginCustomerUser(); + + AiModel model = constructValidModel(); + + // WHEN + ResultActions result = doPost("/api/ai/model", model); + + // THEN + result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission))); + } + + @Test + public void saveAiModel_whenCreatingValidModelAsTenantAdmin_shouldSucceed() throws Exception { + // GIVEN + loginTenantAdmin(); + + AiModel model = constructValidModel(); + + // WHEN + var savedModel = doPost("/api/ai/model", model, AiModel.class); + + // THEN + + // verify returned object + assertThat(savedModel.getId()).isNotNull(); + assertThat(savedModel.getUuidId()).isNotNull().isNotEqualTo(EntityId.NULL_UUID); + assertThat(savedModel.getId().getEntityType()).isEqualTo(EntityType.AI_MODEL); + + assertThat(savedModel.getCreatedTime()).isPositive(); + assertThat(savedModel.getVersion()).isEqualTo(1); + + assertThat(savedModel.getTenantId()).isEqualTo(tenantId); + assertThat(savedModel.getName()).isEqualTo("Test model"); + assertThat(savedModel.getConfiguration()).isEqualTo(model.getConfiguration()); + + assertThat(savedModel.getExternalId()).isNull(); + + // verify auto-commit + then(versionControlService).should().autoCommit( + argThat(actualUser -> Objects.equals(actualUser.getId(), tenantAdminUser.getId())), eq(savedModel.getId()) + ); + + // verify a rule engine message was sent, and an audit log was created + then(logEntityActionService).should().logEntityAction( + eq(tenantId), + eq(savedModel.getId()), + eq(savedModel), + eq(ActionType.ADDED), + argThat(actualUser -> Objects.equals(actualUser.getId(), tenantAdminUser.getId())) + ); + } + + @Test + public void saveAiModel_whenUpdatingExistingModelAsTenantAdmin_shouldSucceedAndLogAction() throws Exception { + // GIVEN + loginTenantAdmin(); + + var model = doPost("/api/ai/model", constructValidModel(), AiModel.class); + + var newModelConfig = OpenAiChatModelConfig.builder() + .providerConfig(new OpenAiProviderConfig("test-api-key-updated")) + .modelId("o4-mini") + .temperature(0.2) + .topP(0.4) + .frequencyPenalty(0.2) + .presencePenalty(0.5) + .maxOutputTokens(2000) + .timeoutSeconds(20) + .maxRetries(0) + .build(); + + model.setName("Test model updated"); + model.setConfiguration(newModelConfig); + + // WHEN + var updatedModel = doPost("/api/ai/model", model, AiModel.class); + + // THEN + + // verify returned object + assertThat(updatedModel.getId()).isEqualTo(model.getId()); + + assertThat(updatedModel.getCreatedTime()).isEqualTo(model.getCreatedTime()); + assertThat(updatedModel.getVersion()).isEqualTo(2); + + assertThat(updatedModel.getTenantId()).isEqualTo(tenantId); + assertThat(updatedModel.getName()).isEqualTo("Test model updated"); + assertThat(updatedModel.getConfiguration()).isEqualTo(newModelConfig); + + assertThat(updatedModel.getExternalId()).isNull(); + + // verify auto-commit + then(versionControlService).should(times(2)).autoCommit( + argThat(actualUser -> Objects.equals(actualUser.getId(), tenantAdminUser.getId())), eq(updatedModel.getId()) + ); + + // verify a rule engine message was sent, and an audit log was created + then(logEntityActionService).should().logEntityAction( + eq(tenantId), eq(updatedModel.getId()), eq(updatedModel), eq(ActionType.UPDATED), + argThat(actualUser -> Objects.equals(actualUser.getId(), tenantAdminUser.getId())) + ); + } + + private AiModel constructValidModel() { + var modelConfig = OpenAiChatModelConfig.builder() + .providerConfig(new OpenAiProviderConfig("test-api-key")) + .modelId("gpt-4o") + .temperature(0.5) + .topP(0.3) + .frequencyPenalty(0.1) + .presencePenalty(0.2) + .maxOutputTokens(1000) + .timeoutSeconds(60) + .maxRetries(2) + .build(); + + return AiModel.builder() + .tenantId(tenantId) + .name("Test model") + .configuration(modelConfig) + .build(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java index 886c020634..7c03a670a7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java @@ -22,10 +22,12 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; +import lombok.Builder; import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; +@Builder public record OpenAiChatModelConfig( @NotNull @Valid OpenAiProviderConfig providerConfig, @NotBlank String modelId, From 64f0da33655ae8d7569b140649bfafff92b9530a Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 10 Jul 2025 13:32:39 +0300 Subject: [PATCH 176/435] UI: ref after review --- ui-ngx/src/app/core/services/menu.models.ts | 2 +- ui-ngx/src/app/modules/common/modules-map.ts | 8 +- .../ai-model/ai-model-dialog.component.html | 41 ++--- .../ai-model/ai-model-dialog.component.scss | 0 .../ai-model/ai-model-dialog.component.ts | 30 +++- .../check-connectivity-dialog.component.html | 9 +- .../check-connectivity-dialog.component.scss | 7 +- .../check-connectivity-dialog.component.ts | 6 +- .../home/components/home-components.module.ts | 3 + .../external/ai-config.component.html | 5 +- .../rule-node/external/ai-config.component.ts | 4 +- .../ai-model/ai-model-table-config.resolve.ts | 9 +- .../models-list-autocomplete.component.html | 41 ----- .../models-list-autocomplete.component.ts | 161 ------------------ .../json-object-edit.component.html | 7 + .../components/json-object-edit.component.ts | 2 + .../string-autocomplete.component.html | 2 +- .../string-autocomplete.component.ts | 3 + ui-ngx/src/app/shared/shared.module.ts | 6 - .../assets/locale/locale.constant-en_US.json | 1 + 20 files changed, 88 insertions(+), 259 deletions(-) rename ui-ngx/src/app/{shared => modules/home}/components/ai-model/ai-model-dialog.component.html (88%) rename ui-ngx/src/app/{shared => modules/home}/components/ai-model/ai-model-dialog.component.scss (100%) rename ui-ngx/src/app/{shared => modules/home}/components/ai-model/ai-model-dialog.component.ts (86%) delete mode 100644 ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.html delete mode 100644 ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index dc568c148e..b660b0ab0d 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -794,6 +794,7 @@ const defaultUserMenuMap = new Map([ {id: MenuId.home}, {id: MenuId.alarms}, {id: MenuId.dashboards}, + {id: MenuId.ai_models}, { id: MenuId.entities, pages: [ @@ -852,7 +853,6 @@ const defaultUserMenuMap = new Map([ {id: MenuId.notification_rules} ] }, - {id: MenuId.ai_models}, { id: MenuId.mobile_center, pages: [ diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 60a26e4211..0890b9e623 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -336,8 +336,7 @@ import * as DatapointsLimitComponent from '@shared/components/time/datapoints-li import * as AggregationTypeSelectComponent from '@shared/components/time/aggregation/aggregation-type-select.component'; import * as AggregationOptionsConfigComponent from '@shared/components/time/aggregation/aggregation-options-config-panel.component'; import * as IntervalOptionsConfigPanelComponent from '@shared/components/time/interval-options-config-panel.component'; -import * as AIModelDialogComponent from '@shared/components/ai-model/ai-model-dialog.component'; -import * as ModelsListAutocompleteComponent from '@shared/components/ai-model/models-list-autocomplete.component'; +import * as AIModelDialogComponent from '@home/components/ai-model/ai-model-dialog.component'; import { IModulesMap } from '@modules/common/modules-map.models'; import { Observable, of } from 'rxjs'; @@ -534,8 +533,6 @@ class ModulesMap implements IModulesMap { '@shared/components/image/gallery-image-input.component': GalleryImageInputComponent, '@shared/components/image/multiple-gallery-image-input.component': MultipleGalleryImageInputComponent, '@shared/components/popover.service': TbPopoverService, - '@shared/components/ai-model/ai-model-dialog.component': AIModelDialogComponent, - '@shared/components/ai-model/models-list-autocomplete.component': ModelsListAutocompleteComponent, '@home/components/alarm/alarm-filter-config.component': AlarmFilterConfigComponent, @@ -672,7 +669,8 @@ class ModulesMap implements IModulesMap { '@home/components/dashboard-page/dashboard-image-dialog.component': DashboardImageDialogComponent, '@home/components/widget/widget-container.component': WidgetContainerComponent, '@home/components/profile/queue/tenant-profile-queues.component': TenantProfileQueuesComponent, - '@home/components/queue/queue-form.component': QueueFormComponent + '@home/components/queue/queue-form.component': QueueFormComponent, + '@home/components/ai-model/ai-model-dialog.component': AIModelDialogComponent, }; init(): Observable { diff --git a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html similarity index 88% rename from ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html rename to ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html index 2a5a34a2c9..4201c5a418 100644 --- a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html @@ -56,7 +56,7 @@
- @if (AiModelMap.get(provider).providerFieldsList.includes('personalAccessToken')) { + @if (providerFieldsList.includes('personalAccessToken')) { ai-models.personal-access-token @@ -66,7 +66,7 @@ } - @if (AiModelMap.get(provider).providerFieldsList.includes('projectId')) { + @if (providerFieldsList.includes('projectId')) { ai-models.project-id @@ -75,7 +75,7 @@ } - @if (AiModelMap.get(provider).providerFieldsList.includes('location')) { + @if (providerFieldsList.includes('location')) { ai-models.location @@ -84,7 +84,7 @@ } - @if (AiModelMap.get(provider).providerFieldsList.includes('serviceAccountKey')) { + @if (providerFieldsList.includes('serviceAccountKey')) { } - @if (AiModelMap.get(provider).providerFieldsList.includes('endpoint')) { + @if (providerFieldsList.includes('endpoint')) { ai-models.endpoint @@ -106,13 +106,13 @@ } - @if (AiModelMap.get(provider).providerFieldsList.includes('serviceVersion')) { + @if (providerFieldsList.includes('serviceVersion')) { ai-models.service-version } - @if (AiModelMap.get(provider).providerFieldsList.includes('apiKey')) { + @if (providerFieldsList.includes('apiKey')) { ai-models.api-key @@ -129,14 +129,17 @@
ai-models.configuration
- - + +
- @if (AiModelMap.get(provider).modelFieldsList.includes('temperature')) { + @if (modelFieldsList.includes('temperature')) {
{{ 'ai-models.temperature' | translate }} @@ -155,7 +158,7 @@
} - @if (AiModelMap.get(provider).modelFieldsList.includes('topP')) { + @if (modelFieldsList.includes('topP')) {
{{ 'ai-models.top-p' | translate }} @@ -175,7 +178,7 @@
} - @if (AiModelMap.get(provider).modelFieldsList.includes('topK')) { + @if (modelFieldsList.includes('topK')) {
{{ 'ai-models.top-k' | translate }} @@ -194,7 +197,7 @@
} - @if (AiModelMap.get(provider).modelFieldsList.includes('presencePenalty')) { + @if (modelFieldsList.includes('presencePenalty')) {
{{ 'ai-models.presence-penalty' | translate }} @@ -206,7 +209,7 @@
} - @if (AiModelMap.get(provider).modelFieldsList.includes('frequencyPenalty')) { + @if (modelFieldsList.includes('frequencyPenalty')) {
{{ 'ai-models.frequency-penalty' | translate }} @@ -217,7 +220,7 @@
} - @if (AiModelMap.get(provider).modelFieldsList.includes('maxOutputTokens')) { + @if (modelFieldsList.includes('maxOutputTokens')) {
{{ 'ai-models.max-output-token' | translate }} diff --git a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.scss b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.scss similarity index 100% rename from ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.scss rename to ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.scss diff --git a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts similarity index 86% rename from ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts rename to ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts index d9ae3eaa69..a5fca8d122 100644 --- a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts @@ -20,7 +20,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { StepperOrientation } from '@angular/cdk/stepper'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { EntityType } from '@shared/models/entity-type.models'; @@ -35,6 +35,7 @@ import { } from '@shared/models/ai-model.models'; import { AiModelService } from '@core/http/ai-model.service'; import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component'; +import { map } from 'rxjs/operators'; export interface AIModelDialogData { AIModel?: AiModel; @@ -112,7 +113,7 @@ export class AIModelDialogComponent extends DialogComponent { this.provider = provider; - this.aiModelForms.get('configuration.modelId').reset({}); + this.aiModelForms.get('configuration.modelId').reset(''); this.aiModelForms.get('configuration.providerConfig').reset({}); this.updateValidation(provider); }) @@ -120,11 +121,28 @@ export class AIModelDialogComponent extends DialogComponent> { + const search = searchText ? searchText?.toLowerCase() : ''; + return of(this.provider ? AiModelMap.get(this.provider).modelList || [] : []).pipe( + map(name => name?.filter(option => option.toLowerCase().includes(search))), + ); + } + private updateValidation(provider: AiProvider) { - ProviderFieldsAllList.forEach(key => - this.aiModelForms.get('configuration.providerConfig') - .get(key)[AiModelMap.get(provider).providerFieldsList.includes(key) ? 'enable' : 'disable']() - ) + ProviderFieldsAllList.forEach(key => { + if (AiModelMap.get(provider).providerFieldsList.includes(key)) { + this.aiModelForms.get('configuration.providerConfig').get(key).enable(); + } else { + this.aiModelForms.get('configuration.providerConfig').get(key).disable(); + } + }) + } + + get providerFieldsList(): string[] { + return AiModelMap.get(this.provider).providerFieldsList; + } + get modelFieldsList(): string[] { + return AiModelMap.get(this.provider).modelFieldsList; } cancel(): void { diff --git a/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html index 9ead874275..afc881b10b 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html @@ -15,15 +15,15 @@ limitations under the License. --> -
-

ai-models.check-connectivity

+ +

ai-models.check-connectivity

-
+
-
- +
@@ -84,6 +84,7 @@ @if (aiConfigForm.get('responseFormat.type').value === responseFormat.JSON_SCHEMA) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts index 3b7f0a92d6..3c9d8bf919 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts @@ -19,7 +19,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; import { EntityType } from '@shared/models/entity-type.models'; import { MatDialog } from '@angular/material/dialog'; -import { AIModelDialogComponent, AIModelDialogData } from '@shared/components/ai-model/ai-model-dialog.component'; +import { AIModelDialogComponent, AIModelDialogData } from '@home/components/ai-model/ai-model-dialog.component'; import { AiModel, AiRuleNodeResponseFormatTypeOnlyText, ResponseFormat } from '@shared/models/ai-model.models'; import { deepTrim } from '@core/utils'; @@ -47,7 +47,7 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { protected onConfigurationSet(configuration: RuleNodeConfiguration) { this.aiConfigForm = this.fb.group({ - modelSettingsId: [configuration?.modelSettingsId ?? null, [Validators.required]], + modelId: [configuration?.modelId ?? null, [Validators.required]], systemPrompt: [configuration?.systemPrompt ?? '', [Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], userPrompt: [configuration?.userPrompt ?? '', [Validators.required, Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], responseFormat: this.fb.group({ diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts index 3f3ded2abb..03db209ada 100644 --- a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts @@ -31,7 +31,7 @@ import { Observable } from 'rxjs'; import { AiModel, AiProviderTranslations } from '@shared/models/ai-model.models'; import { AiModelService } from '@core/http/ai-model.service'; import { AiModelTableHeaderComponent } from '@home/pages/ai-model/ai-model-table-header.component'; -import { AIModelDialogComponent, AIModelDialogData } from '@shared/components/ai-model/ai-model-dialog.component'; +import { AIModelDialogComponent, AIModelDialogData } from '@home/components/ai-model/ai-model-dialog.component'; import { map } from 'rxjs/operators'; @Injectable() @@ -81,7 +81,7 @@ export class AiModelsTableConfigResolver { this.config.cellActionDescriptors = this.configureCellActions(); this.config.handleRowClick = ($event, model) => { - this.editModel(model); + this.editModel($event, model); return true; }; } @@ -96,12 +96,13 @@ export class AiModelsTableConfigResolver { name: this.translate.instant('action.edit'), icon: 'edit', isEnabled: () => true, - onAction: ($event, entity) => this.editModel(entity) + onAction: ($event, entity) => this.editModel($event, entity) } ]; } - private editModel(AIModel: AiModel): void { + private editModel($event, AIModel: AiModel): void { + $event?.stopPropagation(); this.addModel(AIModel, false).subscribe(); } diff --git a/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.html b/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.html deleted file mode 100644 index d204013805..0000000000 --- a/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.html +++ /dev/null @@ -1,41 +0,0 @@ - - - {{label}} - - - - {{errorText}} - - - - - - - diff --git a/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts b/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts deleted file mode 100644 index 3e3c0cbb66..0000000000 --- a/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts +++ /dev/null @@ -1,161 +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. -/// - -import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormControl, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; -import { Observable } from 'rxjs'; -import { map, startWith, tap } from 'rxjs/operators'; -import { TranslateService } from '@ngx-translate/core'; -import { coerceBoolean } from '@shared/decorators/coercion'; -import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; -import { AiModelMap, AiProvider } from '@shared/models/ai-model.models'; - -@Component({ - selector: 'tb-models-list-autocomplete', - templateUrl: './models-list-autocomplete.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ModelsListAutocompleteComponent), - multi: true - } - ] -}) -export class ModelsListAutocompleteComponent implements ControlValueAccessor, OnInit, OnChanges { - - @ViewChild('nameInput', {static: true}) nameInput: ElementRef; - - @Input() - disabled: boolean; - - @Input() - @coerceBoolean() - required = false; - - @Input() - provider: AiProvider; - - @Input() - placeholderText: string = this.translate.instant('widget-config.set'); - - @Input() - subscriptSizing: SubscriptSizing = 'dynamic'; - - @Input() - appearance: MatFormFieldAppearance = 'outline'; - - @Input() - label: string; - - @Input() - errorText: string; - - selectionFormControl: FormControl; - modelValue: string | null; - - filteredOptions$: Observable>; - - searchText = ''; - - private dirty = false; - - private propagateChange = (_val: any) => {}; - - constructor(private fb: FormBuilder, - private translate: TranslateService) { - } - - ngOnInit() { - this.selectionFormControl = this.fb.control('', this.required ? [Validators.required] : []); - this.setupFilteredOptions(); - } - - ngOnChanges(changes: SimpleChanges) { - if (changes.provider && !changes.provider.isFirstChange()) { - this.setupFilteredOptions(); - this.selectionFormControl.setValue(null, {emitEvent: false}); - this.modelValue = null; - this.propagateChange(null); - } - } - - private setupFilteredOptions() { - this.filteredOptions$ = this.selectionFormControl.valueChanges.pipe( - startWith(''), - tap(value => this.updateView(value)), - map(value => { - const search = value ? value.toLowerCase() : ''; - const options = this.provider ? AiModelMap.get(this.provider).modelList || [] : []; - return search ? options.filter(option => option.toLowerCase().includes(search)) : options; - }) - ); - } - - writeValue(option?: string): void { - this.searchText = ''; - this.modelValue = option ? option : null; - - if (option) { - this.selectionFormControl.patchValue(option, { emitEvent: false }); - this.dirty = true; - } else { - this.selectionFormControl.patchValue(null, { emitEvent: false }); - this.dirty = true; - } - } - - onFocus() { - if (this.dirty) { - this.selectionFormControl.updateValueAndValidity({onlySelf: true, emitEvent: true}); - this.dirty = false; - } - } - - updateView(value: string) { - this.searchText = value ? value : ''; - if (this.modelValue !== value && value) { - this.modelValue = value; - this.propagateChange(this.modelValue); - } - } - - registerOnChange(fn: any): void { - this.propagateChange = fn; - } - - registerOnTouched(fn: any): void { - } - - setDisabledState(isDisabled: boolean): void { - this.disabled = isDisabled; - if (this.disabled) { - this.selectionFormControl.disable({emitEvent: false}); - } else { - this.selectionFormControl.enable({emitEvent: false}); - } - } - - clear() { - this.selectionFormControl.patchValue(null, {emitEvent: true}); - this.propagateChange(null); - this.modelValue = null; - setTimeout(() => { - this.nameInput.nativeElement.blur(); - this.nameInput.nativeElement.focus(); - }, 0); - } -} diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.html b/ui-ngx/src/app/shared/components/json-object-edit.component.html index 70967d3483..608efd8ad4 100644 --- a/ui-ngx/src/app/shared/components/json-object-edit.component.html +++ b/ui-ngx/src/app/shared/components/json-object-edit.component.html @@ -32,6 +32,13 @@ mat-button *ngIf="!readonly && !disabled" class="tidy" (click)="minifyJSON()"> {{'js-func.mini' | translate }} + @if (iconHint) { + + }