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;
+ }
+
+}