From cf3b8b81ad25f7c4ab55ba2bc98846f76f9005bf Mon Sep 17 00:00:00 2001 From: Yevhen Popok Date: Wed, 17 Jan 2024 11:57:14 +0200 Subject: [PATCH 001/235] 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/235] 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/235] 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 80284ad7c03f51e88f0f538602cfb5376d1cd4a1 Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 23 Jan 2025 16:34:14 +0200 Subject: [PATCH 004/235] sparkplug: ver 3.0 --- .../AbstractMqttV5ClientSparkplugTest.java | 19 +- ...ctMqttV5ClientSparkplugAttributesTest.java | 73 +++++- .../MqttV5ClientSparkplugBAttributesTest.java | 4 + ...ctMqttV5ClientSparkplugConnectionTest.java | 6 +- .../rpc/AbstractMqttV5RpcSparkplugTest.java | 7 +- ...actMqttV5ClientSparkplugTelemetryTest.java | 6 +- .../transport/mqtt/MqttTransportHandler.java | 161 ++++++++++-- .../AbstractGatewaySessionHandler.java | 13 + .../SparkplugDeviceSessionContext.java | 58 ++-- .../session/SparkplugNodeSessionHandler.java | 99 ++++--- .../mqtt/util/sparkplug/DeviceDescriptor.java | 77 ++++++ .../util/sparkplug/EdgeNodeDescriptor.java | 80 ++++++ .../mqtt/util/sparkplug/MetricDataType.java | 49 +++- .../util/sparkplug/SparkplugDescriptor.java | 26 ++ .../util/sparkplug/SparkplugMessageType.java | 21 +- .../util/sparkplug/SparkplugMetricUtil.java | 9 + .../mqtt/util/sparkplug/SparkplugTopic.java | 247 +++++++++++++++--- .../util/sparkplug/SparkplugTopicService.java | 67 +++++ .../util/sparkplug/SparkplugTopicUtil.java | 116 -------- .../mqtt/util/sparkplug/SpecVersion.java | 27 ++ .../mqtt/src/main/proto/sparkplug.proto | 238 +++++++++-------- 21 files changed, 1024 insertions(+), 379 deletions(-) create mode 100644 common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/DeviceDescriptor.java create mode 100644 common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/EdgeNodeDescriptor.java create mode 100644 common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugDescriptor.java create mode 100644 common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicService.java delete mode 100644 common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicUtil.java create mode 100644 common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SpecVersion.java diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java index 184e262f82..f10621842f 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java @@ -67,7 +67,7 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataTyp import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt64; import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt8; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.createMetric; -import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_ROOT_SPB_V_1_0; /** * Created by nickAS21 on 12.01.23 @@ -114,14 +114,14 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte } public void clientWithCorrectNodeAccessTokenWithNDEATH(long ts, long value) throws Exception { - IMqttToken connectionResult = clientConnectWithNDEATH(ts, value); + IMqttToken connectionResult = clientMqttV5ConnectWithNDEATH(ts, value); MqttWireMessage response = connectionResult.getResponse(); Assert.assertEquals(MESSAGE_TYPE_CONNACK, response.getType()); MqttConnAck connAckMsg = (MqttConnAck) response; Assert.assertEquals(MqttReturnCode.RETURN_CODE_SUCCESS, connAckMsg.getReturnCode()); } - public IMqttToken clientConnectWithNDEATH(long ts, long value, String... nameSpaceBad) throws Exception { + public IMqttToken clientMqttV5ConnectWithNDEATH(long ts, long value, String... nameSpaceBad) throws Exception { String key = keysBdSeq; MetricDataType metricDataType = Int64; SparkplugBProto.Payload.Builder deathPayload = SparkplugBProto.Payload.newBuilder() @@ -132,11 +132,16 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte this.mqttCallback = new SparkplugMqttCallback(); this.client.setCallback(this.mqttCallback); MqttConnectionOptions options = new MqttConnectionOptions(); + // If the MQTT client is using MQTT v5.0, the Edge Node’s MQTT CONNECT packet MUST set the Clean Start flag to true and the Session Expiry Interval to 0 + options.setCleanStart(true); + options.setSessionExpiryInterval(0L); options.setUserName(gatewayAccessToken); - String nameSpace = nameSpaceBad.length == 0 ? NAMESPACE : nameSpaceBad[0]; + String nameSpace = nameSpaceBad.length == 0 ? TOPIC_ROOT_SPB_V_1_0 : nameSpaceBad[0]; String topic = nameSpace + "/" + groupId + "/" + SparkplugMessageType.NDEATH.name() + "/" + edgeNode; + // The NDEATH message MUST set the MQTT Will QoS to 1 and Retained flag to false MqttMessage msg = new MqttMessage(); msg.setId(0); + msg.setQos(1); msg.setPayload(deathBytes); options.setWill(topic, msg); return client.connect(options); @@ -155,7 +160,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte payloadBirthNode.addMetrics(metric); payloadBirthNode.setTimestamp(ts); if (client.isConnected()) { - client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode, + client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode, payloadBirthNode.build().toByteArray(), 0, false); } @@ -169,7 +174,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte payloadBirthDevice.addMetrics(metric); if (client.isConnected()) { - client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.DBIRTH.name() + "/" + edgeNode + "/" + deviceName, + client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.DBIRTH.name() + "/" + edgeNode + "/" + deviceName, payloadBirthDevice.build().toByteArray(), 0, false); AtomicReference device = new AtomicReference<>(); await(alias + "find device [" + deviceName + "] after created") @@ -216,7 +221,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte listKeys.add(metricKey); if (client.isConnected()) { - client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode, + client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode, payloadBirthNode.build().toByteArray(), 0, false); } return listKeys; diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java index 70261f02d7..09f0b6df47 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.transport.mqtt.sparkplug.attributes; import com.fasterxml.jackson.core.type.TypeReference; -import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.thingsboard.server.common.data.Device; @@ -33,8 +32,10 @@ import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt32; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DCMD; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DDATA; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NCMD; -import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NDATA; /** * Created by nickAS21 on 12.01.23 @@ -50,7 +51,6 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra Assert.assertTrue(listKeys.contains(keyNodeRebirth)); String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + keyNodeRebirth + "\":" + value + "}"; Assert.assertTrue("Connection node is failed", client.isConnected()); - client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1); doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); await(alias + SparkplugMessageType.NBIRTH.name()) @@ -76,7 +76,6 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra Object metricValue = nextBoolean(); connectionWithNBirth(metricDataType, metricKey, metricValue); Assert.assertTrue("Connection node is failed", client.isConnected()); - client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1); // Boolean <-> String @@ -140,7 +139,6 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra Object metricValue = nextUInt32(); connectionWithNBirth(metricDataType, metricKey, metricValue); Assert.assertTrue("Connection node is failed", client.isConnected()); - client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1); // Long <-> String @@ -192,7 +190,6 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra Object metricValue = nextFloat(30, 400); connectionWithNBirth(metricDataType, metricKey, metricValue); Assert.assertTrue("Connection node is failed", client.isConnected()); - client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1); // Float <-> String @@ -237,6 +234,67 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getFloatValue()); } + protected void processClientWithCorrectAccessTokenPublishMetricDataTypeFromJson_SendValueOk() throws Exception { + long ts = calendar.getTimeInMillis(); + List devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts); + awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1); + awaitForDeviceActorToReceiveSubscription(devices.get(0).getId(), FeatureType.ATTRIBUTES, 1); + + // Node Edge + SparkplugMessageType messageType = NCMD; + String keyBirthNameNode = "Node Metric int32"; + int valueBirthNameNode = 1024; + String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + messageType.name() + "\": {\"" + keyBirthNameNode + "\":" + valueBirthNameNode + "}}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + NCMD.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(keyBirthNameNode, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(valueBirthNameNode == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + messageType = NDATA; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + messageType.name() + "\": {\"" + keyBirthNameNode + "\":" + valueBirthNameNode + "}}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + NCMD.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(keyBirthNameNode, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(valueBirthNameNode == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + // Device + messageType = DCMD; + String keyBirthNameDevice = metricBirthName_Int32; + int valueBirthNameDevice = 123456; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + messageType.name() + "\": {\"" + keyBirthNameDevice + "\":" + valueBirthNameDevice + "}}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + DCMD.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(keyBirthNameDevice, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(valueBirthNameDevice == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + messageType = DDATA; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + messageType.name() + "\": {\"" + keyBirthNameDevice + "\":" + valueBirthNameDevice + "}}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + DCMD.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(keyBirthNameDevice, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(valueBirthNameDevice == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + } + protected void processClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { clientWithCorrectNodeAccessTokenWithNDEATH(); MetricDataType metricDataType = MetricDataType.Double; @@ -244,7 +302,6 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra Object metricValue = nextDouble(); connectionWithNBirth(metricDataType, metricKey, metricValue); Assert.assertTrue("Connection node is failed", client.isConnected()); - client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1); // Double <-> String @@ -296,7 +353,6 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra Object metricValue = nextString(); connectionWithNBirth(metricDataType, metricKey, metricValue); Assert.assertTrue("Connection node is failed", client.isConnected()); - client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1); // String <-> Long @@ -356,7 +412,6 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra return mqttCallback.getMessageArrivedMetrics().size() == 1; }); Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); - Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); Assert.assertEquals(expectedValueInt, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java index c5518c55fb..58633309af 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java @@ -57,6 +57,10 @@ public class MqttV5ClientSparkplugBAttributesTest extends AbstractMqttV5ClientSp public void testClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { processClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk(); } + @Test + public void testClientWithCorrectAccessTokenPublishMetricDataTypeFromJson_SendValueOk() throws Exception { + processClientWithCorrectAccessTokenPublishMetricDataTypeFromJson_SendValueOk(); + } @Test public void testClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java index 561594f0bf..dee295604c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java @@ -40,7 +40,7 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConn import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.ONLINE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.messageName; -import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_ROOT_SPB_V_1_0; /** * Created by nickAS21 on 12.01.23 @@ -80,7 +80,7 @@ public abstract class AbstractMqttV5ClientSparkplugConnectionTest extends Abstra protected void processClientWithCorrectNodeAccessTokenNameSpaceInvalid_Test() throws Exception { long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; long value = bdSeq = 0; - MqttException actualException = Assert.assertThrows(MqttException.class, () -> clientConnectWithNDEATH(ts, value, "spBv1.2")); + MqttException actualException = Assert.assertThrows(MqttException.class, () -> clientMqttV5ConnectWithNDEATH(ts, value, "spBv1.2")); String expectedMessage = "Server unavailable."; int expectedReasonCode = 136; Assert.assertEquals(expectedMessage, actualException.getMessage()); @@ -135,7 +135,7 @@ public abstract class AbstractMqttV5ClientSparkplugConnectionTest extends Abstra if (client.isConnected()) { List devicesList = new ArrayList<>(devices); Device device = devicesList.get(indexDeviceDisconnect); - client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.DDEATH.name() + "/" + edgeNode + "/" + device.getName(), + client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.DDEATH.name() + "/" + edgeNode + "/" + device.getName(), payloadDeathDevice.build().toByteArray(), 0, false); await(alias + messageName(STATE) + ", device: " + device.getName()) .atMost(40, TimeUnit.SECONDS) diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java index 05e48453ff..4bec661c7d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java @@ -32,7 +32,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.thingsboard.server.common.data.exception.ThingsboardErrorCode.INVALID_ARGUMENTS; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DCMD; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NCMD; -import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; @Slf4j public abstract class AbstractMqttV5RpcSparkplugTest extends AbstractMqttV5ClientSparkplugTest { @@ -45,7 +44,6 @@ public abstract class AbstractMqttV5RpcSparkplugTest extends AbstractMqttV5Clie clientWithCorrectNodeAccessTokenWithNDEATH(); connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); Assert.assertTrue("Connection node is failed", client.isConnected()); - client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.RPC, 1); String expected = "{\"result\":\"Success: " + SparkplugMessageType.NCMD.name() + "\"}"; String actual = sendRPCSparkplug(NCMD.name(), sparkplugRpcRequest, savedGateway); @@ -63,9 +61,10 @@ public abstract class AbstractMqttV5RpcSparkplugTest extends AbstractMqttV5Clie public void processClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception { long ts = calendar.getTimeInMillis(); List devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts); + awaitForDeviceActorToReceiveSubscription(devices.get(0).getId(), FeatureType.RPC, 1); String expected = "{\"result\":\"Success: " + DCMD.name() + "\"}"; String actual = sendRPCSparkplug(DCMD.name() , sparkplugRpcRequest, devices.get(0)); - await(alias + NCMD.name()) + await(alias + DCMD.name()) .atMost(40, TimeUnit.SECONDS) .until(() -> { return mqttCallback.getMessageArrivedMetrics().size() == 1; @@ -80,7 +79,6 @@ public abstract class AbstractMqttV5RpcSparkplugTest extends AbstractMqttV5Clie clientWithCorrectNodeAccessTokenWithNDEATH(); connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); Assert.assertTrue("Connection node is failed", client.isConnected()); - client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.RPC, 1); String invalidateTypeMessageName = "RCMD"; String expected = "{\"result\":\"" + INVALID_ARGUMENTS + "\",\"error\":\"Failed to convert device RPC command to MQTT msg: " + @@ -94,7 +92,6 @@ public abstract class AbstractMqttV5RpcSparkplugTest extends AbstractMqttV5Clie clientWithCorrectNodeAccessTokenWithNDEATH(); connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); Assert.assertTrue("Connection node is failed", client.isConnected()); - client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.RPC, 1); String metricNameBad = metricBirthName_Int32 + "_Bad"; String sparkplugRpcRequestBad = "{\"metricName\":\"" + metricNameBad + "\",\"value\":" + metricBirthValue_Int32 + "}"; diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java index c637ce5bb1..e2bfb2349e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java @@ -29,7 +29,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import static org.awaitility.Awaitility.await; -import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_ROOT_SPB_V_1_0; /** * Created by nickAS21 on 12.01.23 @@ -67,7 +67,7 @@ public abstract class AbstractMqttV5ClientSparkplugTelemetryTest extends Abstrac createdAddMetricValuePrimitiveTsKv(listTsKvEntry, listKeys, ndataPayload, ts); if (client.isConnected()) { - client.publish(NAMESPACE + "/" + groupId + "/" + messageTypeName + "/" + edgeNode, + client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + messageTypeName + "/" + edgeNode, ndataPayload.build().toByteArray(), 0, false); } @@ -96,7 +96,7 @@ public abstract class AbstractMqttV5ClientSparkplugTelemetryTest extends Abstrac createdAddMetricValueArraysPrimitiveTsKv(listTsKvEntry, listKeys, ndataPayload, ts); if (client.isConnected()) { - client.publish(NAMESPACE + "/" + groupId + "/" + messageTypeName + "/" + edgeNode, + client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + messageTypeName + "/" + edgeNode, ndataPayload.build().toByteArray(), 0, false); } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index e669016514..291eb4489e 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -28,6 +28,8 @@ import io.netty.handler.codec.mqtt.MqttFixedHeader; import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttMessageBuilders; import io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader; +import io.netty.handler.codec.mqtt.MqttProperties.IntegerProperty; +import io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType; import io.netty.handler.codec.mqtt.MqttPubAckMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttQoS; @@ -84,6 +86,7 @@ import org.thingsboard.server.transport.mqtt.limits.SessionLimits; import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx; import org.thingsboard.server.transport.mqtt.session.GatewaySessionHandler; import org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher; +import org.thingsboard.server.transport.mqtt.session.SparkplugDeviceSessionContext; import org.thingsboard.server.transport.mqtt.session.SparkplugNodeSessionHandler; import org.thingsboard.server.transport.mqtt.util.ReturnCodeResolver; import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; @@ -101,6 +104,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Map.Entry; import java.util.Optional; import java.util.UUID; import java.util.concurrent.Callable; @@ -120,9 +124,14 @@ import static org.thingsboard.server.common.transport.service.DefaultTransportSe import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG; import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_RPC_ASYNC_MSG; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.OFFLINE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NBIRTH; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NCMD; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NDEATH; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.SPARKPLUG_BD_SEQUENCE_NUMBER_KEY; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.getTsKvProto; -import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.parseTopicPublish; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.getTsKvProtoFromJsonNode; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic.parseTopic; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.parseTopicPublish; /** * @author Andrew Shvayka @@ -444,11 +453,23 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } + /** + * It may be the case that an Edge Node has many dynamic associated devices. + * Publish: spBv1.0/G1/DBIRTH/E1/+ + * Publish: spBv1.0/G1/DDATA/E1/+ + * Publish: spBv1.0/G1/DCMD/E1/+ + * Publish: spBv1.0/G1/DDEATH/E1/+ + * @param ctx + * @param topicName + * @param mqttMsg + */ + private void handleSparkplugPublishMsg(ChannelHandlerContext ctx, String topicName, MqttPublishMessage mqttMsg) { int msgId = mqttMsg.variableHeader().packetId(); try { SparkplugTopic sparkplugTopic = parseTopicPublish(topicName); - if (sparkplugTopic.isNode()) { + boolean isWildcardInPublish = topicName.contains("+"); + if (!isWildcardInPublish && sparkplugTopic.isNode()) { // A node topic SparkplugBProto.Payload sparkplugBProtoNode = SparkplugBProto.Payload.parseFrom(ProtoMqttAdaptor.toBytes(mqttMsg.payload())); switch (sparkplugTopic.getType()) { @@ -457,21 +478,28 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement case NDATA: sparkplugSessionHandler.onAttributesTelemetryProto(msgId, sparkplugBProtoNode, sparkplugTopic); break; + case NDEATH: + if (sparkplugSessionHandler.onValidateNDEATH(sparkplugBProtoNode)) { + doDisconnect(); + break; + } else { + throw new ThingsboardException(SPARKPLUG_BD_SEQUENCE_NUMBER_KEY + " of " + NDEATH.name() + " is not equals " + + SPARKPLUG_BD_SEQUENCE_NUMBER_KEY + " of " + NBIRTH.name(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } default: } } else { // A device topic SparkplugBProto.Payload sparkplugBProtoDevice = SparkplugBProto.Payload.parseFrom(ProtoMqttAdaptor.toBytes(mqttMsg.payload())); - switch (sparkplugTopic.getType()) { - case DBIRTH: - case DCMD: - case DDATA: - sparkplugSessionHandler.onAttributesTelemetryProto(msgId, sparkplugBProtoDevice, sparkplugTopic); - break; - case DDEATH: - sparkplugSessionHandler.onDeviceDisconnect(mqttMsg, sparkplugTopic.getDeviceId()); - break; - default: + if (isWildcardInPublish) { + for (Entry entry : sparkplugSessionHandler.getDevices().entrySet()) { + String deviceName = entry.getKey(); + SparkplugTopic sparkplugTopicDevice = sparkplugTopic; + sparkplugTopicDevice.updateDeviceIdPlus(deviceName); + handleSparkplugPublishDeviceMsg(sparkplugTopicDevice, msgId, mqttMsg, sparkplugBProtoDevice); + } + } else { + handleSparkplugPublishDeviceMsg(sparkplugTopic, msgId, mqttMsg, sparkplugBProtoDevice); } } } catch (RuntimeException e) { @@ -484,6 +512,36 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } + /** + * It may be the case that an Edge Node has many dynamic associated devices. + * Publish: spBv1.0/G1/DBIRTH/E1/+ + * Publish: spBv1.0/G1/DDATA/E1/+ + * Publish: spBv1.0/G1/DCMD/E1/+ + * Publish: spBv1.0/G1/DDEATH/E1/+ + * @param sparkplugTopic + * @param msgId + * @param mqttMsg + * @throws AdaptorException + * @throws ThingsboardException + * @throws InvalidProtocolBufferException + */ + private void handleSparkplugPublishDeviceMsg(SparkplugTopic sparkplugTopic, int msgId, + MqttPublishMessage mqttMsg, SparkplugBProto.Payload sparkplugBProtoDevice) + throws AdaptorException, ThingsboardException, InvalidProtocolBufferException { + // A device topic + switch (sparkplugTopic.getType()) { + case DBIRTH: + case DCMD: + case DDATA: + sparkplugSessionHandler.onAttributesTelemetryProto(msgId, sparkplugBProtoDevice, sparkplugTopic); + break; + case DDEATH: + sparkplugSessionHandler.onDeviceDisconnect(mqttMsg, sparkplugTopic.getDeviceId()); + break; + default: + } + } + private void processDevicePublish(ChannelHandlerContext ctx, MqttPublishMessage mqttMsg, String topicName, int msgId) { try { Matcher fwMatcher; @@ -785,7 +843,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } try { if (sparkplugSessionHandler != null) { - sparkplugSessionHandler.handleSparkplugSubscribeMsg(grantedQoSList, subscription, reqQoS); + sparkplugSessionHandler.handleSparkplugSubscribeMsg(subscription); activityReported = true; } else { switch (topic) { @@ -875,13 +933,22 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement registerSubQoS(topic, grantedQoSList, reqQoS); } - public void processAttributesRpcSubscribeSparkplugNode(List grantedQoSList, MqttQoS reqQoS) { + /** + * 3.0.0 Edge Node Session Establishment: + * ncmd-subscribe + * [tck-id-message-flow-edge-node-ncmd-subscribe] The MQTT client associated with the Edge + * Node MUST subscribe to a topic of the form spBv1.0/group_id/NCMD/edge_node_id where + * group_id is the Sparkplug Group ID and the edge_node_id is the Sparkplug Edge Node ID for + * this Edge Node. It MUST subscribe on this topic with a QoS of 1. + */ + public void processAttributesRpcSubscribeSparkplugNode() { + List grantedQoSList = new ArrayList<>(); transportService.process(TransportProtos.TransportToDeviceActorMsg.newBuilder() .setSessionInfo(deviceSessionCtx.getSessionInfo()) .setSubscribeToAttributes(SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG) .setSubscribeToRPC(SUBSCRIBE_TO_RPC_ASYNC_MSG) .build(), null); - registerSubQoS(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, grantedQoSList, reqQoS); + registerSubQoS(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, grantedQoSList, AT_LEAST_ONCE); } public void registerSubQoS(String topic, List grantedQoSList, MqttQoS reqQoS) { @@ -1178,12 +1245,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private void checkSparkplugNodeSession(MqttConnectMessage connectMessage, ChannelHandlerContext ctx, SessionMetaData sessionMetaData) { try { if (sparkplugSessionHandler == null) { - SparkplugTopic sparkplugTopicNode = validatedSparkplugTopicConnectedNode(connectMessage); - if (sparkplugTopicNode != null) { + SparkplugTopic sparkplugTopic = validatedSparkplugConnectedWillTopic(connectMessage); + if (sparkplugTopic != null) { SparkplugBProto.Payload sparkplugBProtoNode = SparkplugBProto.Payload.parseFrom(connectMessage.payload().willMessageInBytes()); - sparkplugSessionHandler = new SparkplugNodeSessionHandler(this, deviceSessionCtx, sessionId, true, sparkplugTopicNode); - sparkplugSessionHandler.onAttributesTelemetryProto(0, sparkplugBProtoNode, sparkplugTopicNode); + sparkplugSessionHandler = new SparkplugNodeSessionHandler(this, deviceSessionCtx, sessionId, true, sparkplugTopic); + sparkplugSessionHandler.onAttributesTelemetryProto(0, sparkplugBProtoNode, sparkplugTopic); sessionMetaData.setOverwriteActivityTime(true); + // ncmd-subscribe + processAttributesRpcSubscribeSparkplugNode(); } else { log.trace("[{}][{}] Failed to fetch sparkplugDevice connect: sparkplugTopicName without SparkplugMessageType.NDEATH.", sessionId, deviceSessionCtx.getDeviceInfo().getDeviceName()); throw new ThingsboardException("Invalid request body", ThingsboardErrorCode.BAD_REQUEST_PARAMS); @@ -1196,12 +1265,33 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } - private SparkplugTopic validatedSparkplugTopicConnectedNode(MqttConnectMessage connectMessage) throws ThingsboardException { + /** + * The Death Certificate topic and payload described here are not “published” as an MQTT message by a client, + * but provided as parameters within the MQTT CONNECT control packet when this Sparkplug Edge Node first establishes the MQTT Client session. + * - NDEATH message MUST be registered as a Will Message in the MQTT CONNECT packet. + * -- in the MQTT CONNECT packet The NDEATH message MUST set the MQTT Will QoS to 1. + * -- in the MQTT CONNECT packet The NDEATH message MUST set the MQTT Will Retained flag to false. + * -- If the MQTT client is using MQTT v3.1.1, the Edge Node’s MQTT CONNECT packet MUST set the Clean Session flag to true. + * -- If the MQTT client is using MQTT v5.0, the Edge Node’s MQTT CONNECT packet MUST set the Clean Start flag to true and the Session Expiry Interval to 0 + * @param connectMessage + * @return + * @throws ThingsboardException + */ + private SparkplugTopic validatedSparkplugConnectedWillTopic(MqttConnectMessage connectMessage) throws ThingsboardException { if (StringUtils.isNotBlank(connectMessage.payload().willTopic()) && connectMessage.payload().willMessageInBytes() != null && connectMessage.payload().willMessageInBytes().length > 0) { - SparkplugTopic sparkplugTopicNode = parseTopicPublish(connectMessage.payload().willTopic()); + SparkplugTopic sparkplugTopicNode = parseTopic(connectMessage.payload().willTopic()); if (NDEATH.equals(sparkplugTopicNode.getType())) { + if (connectMessage.variableHeader().willQos() != 1 || connectMessage.variableHeader().isWillRetain()) + return null; + if (!connectMessage.variableHeader().isCleanSession()) return null; + int mqttVer = connectMessage.variableHeader().version(); + if (mqttVer == 5) { + Object sessionExpiryIntervalObj = connectMessage.variableHeader().properties().isEmpty() ? null : connectMessage.variableHeader().properties().getProperty(MqttPropertyType.SESSION_EXPIRY_INTERVAL.value()); + Integer sessionExpiryInterval = sessionExpiryIntervalObj == null ? null : ((IntegerProperty) sessionExpiryIntervalObj).value(); + if (sessionExpiryInterval == null || sessionExpiryInterval != 0) return null; + } return sparkplugTopicNode; } } @@ -1300,18 +1390,30 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onAttributeUpdate(UUID sessionId, TransportProtos.AttributeUpdateNotificationMsg notification) { - log.trace("[{}] Received attributes update notification to device", sessionId); try { if (sparkplugSessionHandler != null) { - log.trace("[{}] Received attributes update notification to sparkplug device", sessionId); - notification.getSharedUpdatedList().forEach(tsKvProto -> { - if (sparkplugSessionHandler.getNodeBirthMetrics().containsKey(tsKvProto.getKv().getKey())) { - SparkplugTopic sparkplugTopic = new SparkplugTopic(sparkplugSessionHandler.getSparkplugTopicNode(), - SparkplugMessageType.NCMD); + log.trace("[{}] Received attributes update notification to sparkplug Edge Node", sessionId); + notification.getSharedUpdatedList().forEach(tsKvProtoShared -> { + SparkplugMessageType messageType = NCMD; + TransportProtos.TsKvProto tsKvProto = tsKvProtoShared; + if ("JSON_V".equals(tsKvProtoShared.getKv().getType().name())) { + try { + messageType = SparkplugMessageType.parseMessageType(tsKvProtoShared.getKv().getKey()); + tsKvProto = getTsKvProtoFromJsonNode(JacksonUtil.toJsonNode(tsKvProtoShared.getKv().getJsonV()), tsKvProtoShared.getTs()); + } catch (ThingsboardException e) { + messageType = null; + log.error("Failed attributes update notification to sparkplug Edge Node [{}]. ", sparkplugSessionHandler.getSparkplugTopicNode().getEdgeNodeId(), e); + } + } + if (messageType != null && messageType.isSubscribe() && messageType.isNode() + && sparkplugSessionHandler.getNodeBirthMetrics().containsKey(tsKvProto.getKv().getKey())) { + SparkplugTopic sparkplugTopic = new SparkplugTopic(sparkplugSessionHandler.getSparkplugTopicNode(), messageType); sparkplugSessionHandler.createSparkplugMqttPublishMsg(tsKvProto, sparkplugTopic.toString(), sparkplugSessionHandler.getNodeBirthMetrics().get(tsKvProto.getKv().getKey())) .ifPresent(sparkplugSessionHandler::writeAndFlush); + } else { + log.trace("Failed attributes update notification to sparkplug Edge Node [{}]. ", sparkplugSessionHandler.getSparkplugTopicNode().getEdgeNodeId()); } }); } else { @@ -1320,7 +1422,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement adaptor.convertToPublish(deviceSessionCtx, notification, topic).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); } } catch (Exception e) { - log.trace("[{}] Failed to convert device attributes update to MQTT msg", sessionId, e); + log.trace("[{}] Failed to convert device/Edge Node attributes update to MQTT msg", sessionId, e); } } @@ -1357,7 +1459,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } - private void onGetSessionLimitsRpc(TransportProtos.SessionInfoProto sessionInfo, ChannelHandlerContext ctx, int msgId, TransportProtos.ToServerRpcRequestMsg rpcRequestMsg) { + private void onGetSessionLimitsRpc(TransportProtos.SessionInfoProto sessionInfo, ChannelHandlerContext ctx, int msgId, TransportProtos. + ToServerRpcRequestMsg rpcRequestMsg) { var tenantProfile = context.getTenantProfileCache().get(deviceSessionCtx.getTenantId()); DefaultTenantProfileConfiguration profile = tenantProfile.getDefaultProfileConfiguration(); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java index 2e205ff840..d039fd322a 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -291,6 +291,15 @@ public abstract class AbstractGatewaySessionHandler getDevices () { + return this.devices; + } + private TransportServiceCallback getPubAckCallback(final ChannelHandlerContext ctx, final String deviceName, final int msgId, final T msg) { return new TransportServiceCallback() { @Override diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugDeviceSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugDeviceSessionContext.java index 9f20484672..ccbb0e19b4 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugDeviceSessionContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugDeviceSessionContext.java @@ -35,7 +35,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DCMD; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.getTsKvProto; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.getTsKvProtoFromJsonNode; @Slf4j public class SparkplugDeviceSessionContext extends AbstractGatewayDeviceSessionContext { @@ -51,7 +53,7 @@ public class SparkplugDeviceSessionContext extends AbstractGatewayDeviceSessionC super(parent, deviceInfo, deviceProfile, mqttQoSMap, transportService); } - public Map getDeviceBirthMetrics() { + public Map getDeviceBirthMetrics() { return deviceBirthMetrics; } @@ -64,14 +66,28 @@ public class SparkplugDeviceSessionContext extends AbstractGatewayDeviceSessionC @Override public void onAttributeUpdate(UUID sessionId, TransportProtos.AttributeUpdateNotificationMsg notification) { log.trace("[{}] Received attributes update notification to sparkplug device", sessionId); - notification.getSharedUpdatedList().forEach(tsKvProto -> { - if (getDeviceBirthMetrics().containsKey(tsKvProto.getKv().getKey())) { + notification.getSharedUpdatedList().forEach(tsKvProtoShared -> { + SparkplugMessageType messageType = DCMD; + TransportProtos.TsKvProto tsKvProto = tsKvProtoShared; + if ("JSON_V".equals(tsKvProtoShared.getKv().getType().name())) { + try { + messageType = SparkplugMessageType.parseMessageType(tsKvProtoShared.getKv().getKey()); + tsKvProto = getTsKvProtoFromJsonNode(JacksonUtil.toJsonNode(tsKvProtoShared.getKv().getJsonV()), tsKvProtoShared.getTs()); + } catch (ThingsboardException e) { + messageType = null; + log.error("Failed attributes update notification to sparkplug device [{}]. ", deviceInfo.getDeviceName(), e); + } + } + if (messageType != null && messageType.isSubscribe() && messageType.isDevice() + && getDeviceBirthMetrics().containsKey(tsKvProto.getKv().getKey())) { SparkplugTopic sparkplugTopic = new SparkplugTopic(parent.getSparkplugTopicNode(), - SparkplugMessageType.DCMD, deviceInfo.getDeviceName()); + messageType, deviceInfo.getDeviceName()); parent.createSparkplugMqttPublishMsg(tsKvProto, - sparkplugTopic.toString(), - getDeviceBirthMetrics().get(tsKvProto.getKv().getKey())) + sparkplugTopic.toString(), + getDeviceBirthMetrics().get(tsKvProto.getKv().getKey())) .ifPresent(this.parent::writeAndFlush); + } else { + log.trace("Failed attributes update notification to sparkplug device [{}]. ", deviceInfo.getDeviceName()); } }); } @@ -81,20 +97,22 @@ public class SparkplugDeviceSessionContext extends AbstractGatewayDeviceSessionC log.trace("[{}] Received RPC Request notification to sparkplug device", sessionId); try { SparkplugMessageType messageType = SparkplugMessageType.parseMessageType(rpcRequest.getMethodName()); - SparkplugRpcRequestHeader header = JacksonUtil.fromString(rpcRequest.getParams(), SparkplugRpcRequestHeader.class); - header.setMessageType(messageType.name()); - TransportProtos.TsKvProto tsKvProto = getTsKvProto(header.getMetricName(), header.getValue(), new Date().getTime()); - if (getDeviceBirthMetrics().containsKey(tsKvProto.getKv().getKey())) { - SparkplugTopic sparkplugTopic = new SparkplugTopic(parent.getSparkplugTopicNode(), - messageType, deviceInfo.getDeviceName()); - parent.createSparkplugMqttPublishMsg(tsKvProto, - sparkplugTopic.toString(), - getDeviceBirthMetrics().get(tsKvProto.getKv().getKey())) - .ifPresent(payload -> parent.sendToDeviceRpcRequest(payload, rpcRequest, sessionInfo)); - } else { - parent.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), - ThingsboardErrorCode.BAD_REQUEST_PARAMS, " Failed send To Device Rpc Request: " + - rpcRequest.getMethodName() + ". This device does not have a metricName: [" + tsKvProto.getKv().getKey() + "]"); + if (messageType.isSubscribe()) { + SparkplugRpcRequestHeader header = JacksonUtil.fromString(rpcRequest.getParams(), SparkplugRpcRequestHeader.class); + header.setMessageType(messageType.name()); + TransportProtos.TsKvProto tsKvProto = getTsKvProto(header.getMetricName(), header.getValue(), new Date().getTime()); + if (getDeviceBirthMetrics().containsKey(tsKvProto.getKv().getKey())) { + SparkplugTopic sparkplugTopic = new SparkplugTopic(parent.getSparkplugTopicNode(), + messageType, deviceInfo.getDeviceName()); + parent.createSparkplugMqttPublishMsg(tsKvProto, + sparkplugTopic.toString(), + getDeviceBirthMetrics().get(tsKvProto.getKv().getKey())) + .ifPresent(payload -> parent.sendToDeviceRpcRequest(payload, rpcRequest, sessionInfo)); + } else { + parent.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), + ThingsboardErrorCode.BAD_REQUEST_PARAMS, " Failed send To Device Rpc Request: " + + rpcRequest.getMethodName() + ". This device does not have a metricName: [" + tsKvProto.getKv().getKey() + "]"); + } } } catch (ThingsboardException e) { parent.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java index e06d954678..7af0487b49 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java @@ -18,19 +18,14 @@ package org.thingsboard.server.transport.mqtt.session; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; -import com.google.protobuf.Descriptors; import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage; -import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.codec.mqtt.MqttTopicSubscription; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.ResponseCode; import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.adaptor.AdaptorException; -import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.adaptor.ProtoConverter; import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @@ -41,7 +36,9 @@ import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; import org.thingsboard.server.transport.mqtt.MqttTransportHandler; import org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SpecVersion; import java.util.ArrayList; import java.util.List; @@ -55,15 +52,20 @@ import java.util.stream.Collectors; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.ONLINE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DBIRTH; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NBIRTH; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.parseMessageType; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.SPARKPLUG_BD_SEQUENCE_NUMBER_KEY; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.SPARKPLUG_SEQUENCE_NUMBER_KEY; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.createMetric; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.fromSparkplugBMetricToKeyValueProto; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.validatedValueByTypeMetric; -import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.parseTopicSubscribe; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_SPLIT_REGEXP; /** * Created by nickAS21 on 12.12.22 */ @Slf4j +@SpecVersion(spec = "sparkplug", version = "3.0.0") public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler { @Getter @@ -85,16 +87,11 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler metric))); } - public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { - DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; - byte[] bytes = getBytes(inbound.payload()); - Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor()); - try { - return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor))); - } catch (Exception e) { - log.debug("Failed to decode post telemetry request", e); - throw new AdaptorException(e); - } + + public boolean onValidateNDEATH(SparkplugBProto.Payload sparkplugBProto) throws ThingsboardException { + return sparkplugBProto.getMetricsCount() == 1 && SPARKPLUG_BD_SEQUENCE_NUMBER_KEY.equals(sparkplugBProto.getMetrics(0).getName()) + && this.nodeBirthMetrics.get(SPARKPLUG_BD_SEQUENCE_NUMBER_KEY) != null + && sparkplugBProto.getMetrics(0).getLongValue() == this.nodeBirthMetrics.get(SPARKPLUG_BD_SEQUENCE_NUMBER_KEY).getLongValue(); } public void onAttributesTelemetryProto(int msgId, SparkplugBProto.Payload sparkplugBProto, SparkplugTopic topic) throws AdaptorException, ThingsboardException { @@ -168,19 +165,22 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler grantedQoSList, MqttTopicSubscription subscription, - MqttQoS reqQoS) throws ThingsboardException { - SparkplugTopic sparkplugTopic = parseTopicSubscribe(subscription.topicFilter()); - if (sparkplugTopic.getGroupId() == null) { - // TODO SUBSCRIBE NameSpace - } else if (sparkplugTopic.getType() == null) { - // TODO SUBSCRIBE GroupId - } else if (sparkplugTopic.isNode()) { - // SUBSCRIBE Node - parent.processAttributesRpcSubscribeSparkplugNode(grantedQoSList, reqQoS); + /** + * Subscribe: spBv1.0/STATE/my_primary_hos -> Implemented as status via checkSparkplugNodeSession + * Subscribe: CMD/DATA -> Implemented after connection: SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG/SUBSCRIBE_TO_RPC_ASYNC_MSG + * @param subscription + * @throws ThingsboardException + */ + public void handleSparkplugSubscribeMsg(MqttTopicSubscription subscription) throws ThingsboardException { + String topic = subscription.topicFilter(); + String[] splitTopic = topic.split(TOPIC_SPLIT_REGEXP); + if (STATE.name().equals(splitTopic[1])) { + log.trace("Subscribing on it’s own spBv1.0/STATE/[the Sparkplug Host Application] - Implemented as status via checkSparkplugNodeSession"); + } else if (this.validateTopicDataSubscribe(splitTopic)) { + // TODO if need subscription DATA + log.trace("Subscribing on it’s own [" + topic + "] - Implemented as SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG/SUBSCRIBE_TO_RPC_ASYNC_MSG via checkSparkplugNode/DeviceSession"); } else { - // SUBSCRIBE Device - DO NOTHING, WE HAVE ALREADY SUBSCRIBED. - // TODO: track that node subscribed to # or to particular device. + log.trace("Failed to subscribe to the topic: [" + topic + "]."); } } @@ -192,7 +192,8 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler onDeviceConnectProto(SparkplugTopic topic) throws ThingsboardException { + private ListenableFuture onDeviceConnectProto(SparkplugTopic topic) throws + ThingsboardException { try { String deviceType = this.gateway.getDeviceType() + " device"; return onDeviceConnect(topic.getNodeDeviceName(), deviceType); @@ -202,13 +203,14 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler convertToPostTelemetry(SparkplugBProto.Payload sparkplugBProto, Set attributesMetricNames, String topicTypeName) throws AdaptorException { + private List convertToPostTelemetry(SparkplugBProto.Payload + sparkplugBProto, Set attributesMetricNames, String topicTypeName) throws AdaptorException { try { List msgs = new ArrayList<>(); for (SparkplugBProto.Payload.Metric protoMetric : sparkplugBProto.getMetricsList()) { if (attributesMetricNames == null || !matches(attributesMetricNames, protoMetric)) { long ts = protoMetric.getTimestamp(); - String key = "bdSeq".equals(protoMetric.getName()) ? + String key = SPARKPLUG_BD_SEQUENCE_NUMBER_KEY.equals(protoMetric.getName()) ? topicTypeName + " " + protoMetric.getName() : protoMetric.getName(); Optional keyValueProtoOpt = fromSparkplugBMetricToKeyValueProto(key, protoMetric); keyValueProtoOpt.ifPresent(kvProto -> msgs.add(postTelemetryMsgCreated(kvProto, ts))); @@ -217,7 +219,7 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler getPostAttributeMsg(SparkplugBProto.Payload.Metric protoMetric) throws ThingsboardException { + private Optional getPostAttributeMsg(SparkplugBProto.Payload.Metric + protoMetric) throws ThingsboardException { Optional keyValueProtoOpt = fromSparkplugBMetricToKeyValueProto(protoMetric.getName(), protoMetric); if (keyValueProtoOpt.isPresent()) { TransportProtos.PostAttributeMsg.Builder builder = TransportProtos.PostAttributeMsg.newBuilder(); @@ -304,16 +307,36 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler= 4 && splitTopic.length <= 5 && + splitTopic[0].equals(this.sparkplugTopicNode.getNamespace()) && + splitTopic[1].equals(this.sparkplugTopicNode.getGroupId()) && + splitTopic[3].equals(this.sparkplugTopicNode.getEdgeNodeId())) { + SparkplugMessageType messageType = parseMessageType(splitTopic[2]); + return messageType.isData(); + } + return false; } - } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/DeviceDescriptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/DeviceDescriptor.java new file mode 100644 index 0000000000..02d994ae5e --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/DeviceDescriptor.java @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2024 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.transport.mqtt.util.sparkplug; + +public class DeviceDescriptor extends EdgeNodeDescriptor { + + private final String deviceId; + private final String descriptorString; + + public DeviceDescriptor(String groupId, String edgeNodeId, String deviceId) { + super(groupId, edgeNodeId); + this.deviceId = deviceId; + this.descriptorString = groupId + "/" + edgeNodeId + "/" + deviceId; + } + + public DeviceDescriptor(String descriptorString) { + super(descriptorString.substring(0, descriptorString.lastIndexOf("/"))); + this.deviceId = descriptorString.substring(descriptorString.lastIndexOf("/") + 1); + this.descriptorString = descriptorString; + } + + public DeviceDescriptor(EdgeNodeDescriptor edgeNodeDescriptor, String deviceId) { + super(edgeNodeDescriptor.getGroupId(), edgeNodeDescriptor.getEdgeNodeId()); + this.deviceId = deviceId; + this.descriptorString = edgeNodeDescriptor.getDescriptorString() + "/" + deviceId; + } + + public String getDeviceId() { + return deviceId; + } + + /** + * Returns a {@link String} representing the Device's Descriptor of the form: + * "//". + * + * @return a {@link String} representing the Device's Descriptor. + */ + @Override + public String getDescriptorString() { + return descriptorString; + } + + public String getEdgeNodeDescriptorString() { + return super.getDescriptorString(); + } + + @Override + public int hashCode() { + return this.getDescriptorString().hashCode(); + } + + @Override + public boolean equals(Object object) { + if (object instanceof DeviceDescriptor) { + return this.getDescriptorString().equals(((DeviceDescriptor) object).getDescriptorString()); + } + return this.getDescriptorString().equals(object); + } + + @Override + public String toString() { + return getDescriptorString(); + } +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/EdgeNodeDescriptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/EdgeNodeDescriptor.java new file mode 100644 index 0000000000..250e78ac0e --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/EdgeNodeDescriptor.java @@ -0,0 +1,80 @@ +/** + * Copyright © 2016-2024 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.transport.mqtt.util.sparkplug; + +import com.fasterxml.jackson.annotation.JsonValue; + +public class EdgeNodeDescriptor implements SparkplugDescriptor{ + + private final String groupId; + private final String edgeNodeId; + private final String descriptorString; + + public EdgeNodeDescriptor(String groupId, String edgeNodeId) { + this.groupId = groupId; + this.edgeNodeId = edgeNodeId; + this.descriptorString = groupId + "/" + edgeNodeId; + } + + /** + * Creates and EdgeNodeDescriptor from a {@link String} of the form group_name/edge_node_name + * + * @param descriptorString the {@link String} representation of an EdgeNodeDescriptor + */ + public EdgeNodeDescriptor(String descriptorString) { + String[] tokens = descriptorString.split("/"); + this.groupId = tokens[0]; + this.edgeNodeId = tokens[1]; + this.descriptorString = descriptorString; + } + + public String getGroupId() { + return groupId; + } + + public String getEdgeNodeId() { + return edgeNodeId; + } + + /** + * Returns a {@link String} representing the Edge Node's Descriptor of the form: "/". + * + * @return a {@link String} representing the Edge Node's Descriptor. + */ + @Override + public String getDescriptorString() { + return descriptorString; + } + + @Override + public int hashCode() { + return this.getDescriptorString().hashCode(); + } + + @Override + public boolean equals(Object object) { + if (object instanceof EdgeNodeDescriptor) { + return this.getDescriptorString().equals(((EdgeNodeDescriptor) object).getDescriptorString()); + } + return this.getDescriptorString().equals(object); + } + + @Override + @JsonValue + public String toString() { + return getDescriptorString(); + } +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/MetricDataType.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/MetricDataType.java index 1040216b51..3cf3fc8c04 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/MetricDataType.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/MetricDataType.java @@ -52,7 +52,24 @@ public enum MetricDataType { File(18, SparkplugMetricUtil.File.class), Template(19, SparkplugBProto.Payload.Template.class), - // PropertyValue Types (20 and 21) are NOT metric datatypes + // Additional PropertyValue Types (PropertyValue Types (20 and 21) are NOT metric datatypes) + PropertySet(20, SparkplugBProto.Payload.PropertySet.class), + PropertySetList(21, SparkplugBProto.Payload.PropertySetList.class), + + // Array Types + Int8Array(22, Byte[].class), + Int16Array(23, Short[].class), + Int32Array(24, Integer[].class), + Int64Array(25, Long[].class), + UInt8Array(26, Short[].class), + UInt16Array(27, Integer[].class), + UInt32Array(28, Long[].class), + UInt64Array(29, BigInteger[].class), + FloatArray(30, Float[].class), + DoubleArray(31, Double[].class), + BooleanArray(32, Boolean[].class), + StringArray(33, String[].class), + DateTimeArray(34, Date[].class), // Unknown Unknown(0, Object.class); @@ -140,6 +157,36 @@ public enum MetricDataType { return File; case 19: return Template; + case 20: + return PropertySet; + case 21: + return PropertySetList; + case 22: + return Int8Array; + case 23: + return Int16Array; + case 24: + return Int32Array; + case 25: + return Int64Array; + case 26: + return UInt8Array; + case 27: + return UInt16Array; + case 28: + return UInt32Array; + case 29: + return UInt64Array; + case 30: + return FloatArray; + case 31: + return DoubleArray; + case 32: + return BooleanArray; + case 33: + return StringArray; + case 34: + return DateTimeArray; default: return Unknown; } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugDescriptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugDescriptor.java new file mode 100644 index 0000000000..7f84f4bd1d --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugDescriptor.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2024 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.transport.mqtt.util.sparkplug; + +public interface SparkplugDescriptor { + + /** + * Returns the String representation of this {@link SparkplugDescriptor} + * + * @return the String representation of this {@link SparkplugDescriptor} + */ + public String getDescriptorString(); +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMessageType.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMessageType.java index 03ed915e1a..421d149c84 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMessageType.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMessageType.java @@ -91,10 +91,14 @@ public enum SparkplugMessageType { return STATE.equals(type) ? "sparkplugConnectionState" : type.name(); } + public boolean isState() { + return this.equals(STATE); + } + public boolean isDeath() { return this.equals(DDEATH) || this.equals(NDEATH); } - + public boolean isCommand() { return this.equals(DCMD) || this.equals(NCMD); } @@ -110,4 +114,19 @@ public enum SparkplugMessageType { public boolean isRecord() { return this.equals(DRECORD) || this.equals(NRECORD); } + public boolean isSubscribe() { + return isCommand() || isData() || isRecord(); + } + + public boolean isNode() { + return this.equals(NBIRTH) + || this.equals(NCMD) || this.equals(NDATA) + ||this.equals(NDEATH) || this.equals(NRECORD); + } + public boolean isDevice() { + return this.equals(DBIRTH) + || this.equals(DCMD) || this.equals(DDATA) + ||this.equals(DDEATH) || this.equals(DRECORD); + } + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMetricUtil.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMetricUtil.java index 3d900e51af..bd75945923 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMetricUtil.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMetricUtil.java @@ -45,6 +45,9 @@ import static org.thingsboard.common.util.JacksonUtil.newArrayNode; @Slf4j public class SparkplugMetricUtil { + public static final String SPARKPLUG_SEQUENCE_NUMBER_KEY = "seq"; + public static final String SPARKPLUG_BD_SEQUENCE_NUMBER_KEY = "bdSeq"; + public static Optional fromSparkplugBMetricToKeyValueProto(String key, SparkplugBProto.Payload.Metric protoMetric) throws ThingsboardException { // Check if the null flag has been set indicating that the value is null if (protoMetric.getIsNull()) { @@ -189,6 +192,12 @@ public class SparkplugMetricUtil { return metric; } + public static TransportProtos.TsKvProto getTsKvProtoFromJsonNode(JsonNode kvProto, long ts) throws ThingsboardException { + String kvProtoKey = kvProto.fieldNames().next(); + String kvProtoValue = kvProto.get(kvProtoKey).asText(); + return getTsKvProto(kvProtoKey, kvProtoValue, ts); + } + public static TransportProtos.TsKvProto getTsKvProto(String key, Object value, long ts) throws ThingsboardException { try { TransportProtos.TsKvProto.Builder tsKvProtoBuilder = TransportProtos.TsKvProto.newBuilder(); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopic.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopic.java index e69a8e1202..142d0d4f6b 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopic.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopic.java @@ -15,65 +15,129 @@ */ package org.thingsboard.server.transport.mqtt.util.sparkplug; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; + +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.parseMessageType; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_ROOT_SPB_V_1_0; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_SPLIT_REGEXP; /** * Created by nickAS21 on 12.12.22 + * A Sparkplug MQTT Topic */ @JsonInclude(JsonInclude.Include.NON_NULL) public class SparkplugTopic { /** * The Sparkplug namespace version. - * For the Sparkplug™ B version of the specification, the UTF-8 string constant for the namespace element will be: “spBv1.0” */ - private String namespace; + private final String namespace; + + /** + * The SparkplugDesciptor for this Edge Node or Device + */ + @JsonIgnore + private final SparkplugDescriptor sparkplugDescriptor; + + /** + * The {@link EdgeNodeDescriptor} for this Edge Node or Device + */ + private final EdgeNodeDescriptor edgeNodeDescriptor; /** * The ID of the logical grouping of Edge of Network (EoN) Nodes and devices. */ - private String groupId; + private final String groupId; /** * The ID of the Edge of Network (EoN) Node. */ - private String edgeNodeId; + private final String edgeNodeId; /** * The ID of the device. */ private String deviceId; + /** + * The ID if this is a Sparkplug Host Application topic + */ + private final String hostApplicationId; + /** * The message type. */ - private SparkplugMessageType type; + private final SparkplugMessageType type; + + public SparkplugTopic() { + this.namespace = null; + this.sparkplugDescriptor = null; + this.edgeNodeDescriptor = null; + this.groupId = null; + this.edgeNodeId = null; + this.deviceId = null; + this.hostApplicationId = null; + this.type = null; + } + + public SparkplugTopic(SparkplugTopic sparkplugTopic, SparkplugMessageType type) { + super(); + this.namespace = sparkplugTopic.namespace; + this.groupId = sparkplugTopic.groupId; + this.edgeNodeId = sparkplugTopic.edgeNodeId; + this.sparkplugDescriptor = new EdgeNodeDescriptor(groupId, edgeNodeId); + this.edgeNodeDescriptor = new EdgeNodeDescriptor(groupId, edgeNodeId); + this.deviceId = null; + this.type = type; + this.hostApplicationId = null; + } - /** - * Constructor (device). + public SparkplugTopic(SparkplugTopic sparkplugTopic, SparkplugMessageType type, String deviceId) { + super(); + this.namespace = sparkplugTopic.namespace; + this.groupId = sparkplugTopic.groupId; + this.edgeNodeId = sparkplugTopic.edgeNodeId; + this.deviceId = deviceId; + this.sparkplugDescriptor = deviceId == null + ? new EdgeNodeDescriptor(groupId, edgeNodeId) + : new DeviceDescriptor(groupId, edgeNodeId, deviceId); + this.edgeNodeDescriptor = new EdgeNodeDescriptor(groupId, edgeNodeId); + this.type = type; + this.hostApplicationId = null; + } + + /** + * A Constructor for Device Topics * - * @param namespace the namespace. - * @param groupId the group ID. - * @param edgeNodeId the edge node ID. - * @param deviceId the device ID. - * @param type the message type. + * @param namespace the namespace + * @param groupId the Group ID + * @param edgeNodeId the Edge Node ID + * @param deviceId the Device ID + * @param type the message type */ public SparkplugTopic(String namespace, String groupId, String edgeNodeId, String deviceId, SparkplugMessageType type) { super(); this.namespace = namespace; + this.sparkplugDescriptor = deviceId == null + ? new EdgeNodeDescriptor(groupId, edgeNodeId) + : new DeviceDescriptor(groupId, edgeNodeId, deviceId); + this.edgeNodeDescriptor = new EdgeNodeDescriptor(groupId, edgeNodeId); this.groupId = groupId; this.edgeNodeId = edgeNodeId; this.deviceId = deviceId; + this.hostApplicationId = null; this.type = type; } /** - * Constructor (node). - * - * @param namespace the namespace. - * @param groupId the group ID. - * @param edgeNodeId the edge node ID. - * @param type the message type. + * A Constructor for Edge Node Topics + * @param namespace the namespace + * @param groupId the group ID + * @param edgeNodeId the edge node ID + * @param type the message type */ public SparkplugTopic(String namespace, String groupId, String edgeNodeId, SparkplugMessageType type) { super(); @@ -81,33 +145,110 @@ public class SparkplugTopic { this.groupId = groupId; this.edgeNodeId = edgeNodeId; this.deviceId = null; + this.sparkplugDescriptor = new EdgeNodeDescriptor(groupId, edgeNodeId); + this.edgeNodeDescriptor = new EdgeNodeDescriptor(groupId, edgeNodeId); + this.hostApplicationId = null; this.type = type; } - public SparkplugTopic(SparkplugTopic sparkplugTopic, SparkplugMessageType type) { - super(); - this.namespace = sparkplugTopic.namespace; - this.groupId = sparkplugTopic.groupId; - this.edgeNodeId = sparkplugTopic.edgeNodeId; - this.deviceId = null; - this.type = type; + /** + * A Constructor for Device Topics + * + * @param namespace the namespace + * @param deviceDescriptor the {@link EdgeNodeDescriptor} + * @param type the message type + */ + public SparkplugTopic(String namespace, DeviceDescriptor deviceDescriptor, SparkplugMessageType type) { + this(namespace, deviceDescriptor.getGroupId(), deviceDescriptor.getEdgeNodeId(), deviceDescriptor.getDeviceId(), + type); + } + + /** + * A Constructor for Edge Node Topics + * + * @param namespace the namespace + * @param edgeNodeDescriptor the {@link EdgeNodeDescriptor} + * @param type the message type + */ + public SparkplugTopic(String namespace, EdgeNodeDescriptor edgeNodeDescriptor, SparkplugMessageType type) { + this(namespace, edgeNodeDescriptor.getGroupId(), edgeNodeDescriptor.getEdgeNodeId(), type); } - public SparkplugTopic(SparkplugTopic sparkplugTopic, SparkplugMessageType type, String deviceId) { + + /** + * A Constructor for Host Application Topics + * + * @param namespace the namespace + * @param hostApplicationId the Host Application ID + */ + public SparkplugTopic(String namespace, String hostApplicationId, SparkplugMessageType type) { super(); - this.namespace = sparkplugTopic.namespace; - this.groupId = sparkplugTopic.groupId; - this.edgeNodeId = sparkplugTopic.edgeNodeId; - this.deviceId = deviceId; + this.namespace = namespace; + this.hostApplicationId = hostApplicationId; this.type = type; + this.sparkplugDescriptor = null; + this.edgeNodeDescriptor = null; + this.groupId = null; + this.edgeNodeId = null; + this.deviceId = null; + } + + public static SparkplugTopic parseTopic(String topicString) throws ThingsboardException { + try { + if (isValidIdElementToUTF8(topicString)) { + SparkplugMessageType messageType; + String[] splitTopic = topicString.split(TOPIC_SPLIT_REGEXP); + if (TOPIC_ROOT_SPB_V_1_0.equals(splitTopic[0])) { + if (splitTopic.length == 3) { + messageType = parseMessageType(splitTopic[1]); + if (messageType.isState()) + return new SparkplugTopic(TOPIC_ROOT_SPB_V_1_0, splitTopic[2], messageType); + } else if (splitTopic.length == 4) { + messageType = parseMessageType(splitTopic[2]); + if (messageType.isNode()) + return new SparkplugTopic(TOPIC_ROOT_SPB_V_1_0, splitTopic[1], splitTopic[3], messageType); + } else if (splitTopic.length == 5) { + messageType = parseMessageType(splitTopic[2]); + if (messageType.isDevice()) + return new SparkplugTopic(TOPIC_ROOT_SPB_V_1_0, splitTopic[1], splitTopic[3], splitTopic[4], messageType); + + } + } + } + throw new ThingsboardException("Invalid Sparkplug topic from String: " + topicString, ThingsboardErrorCode.INVALID_ARGUMENTS); + } catch ( + Exception e) { + throw new ThingsboardException(e, ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + } /** - * @return the Sparkplug namespace version + * Returns the Sparkplug namespace version. + * + * @return the namespace */ public String getNamespace() { return namespace; } + /** + * Returns the {@link SparkplugDescriptor} + * + * @return the SparkplugDescriptor + */ + public SparkplugDescriptor getSparkplugDescriptor() { + return sparkplugDescriptor; + } + + /** + * Returns the {@link EdgeNodeDescriptor} + * + * @return the EdgeNodeDescriptor + */ + public EdgeNodeDescriptor getEdgeNodeDescriptor() { + return edgeNodeDescriptor; + } + /** * Returns the ID of the logical grouping of Edge of Network (EoN) Nodes and devices. * @@ -118,20 +259,39 @@ public class SparkplugTopic { } /** - * @return the ID of the Edge of Network (EoN) Node + * Returns the ID of the Edge of Network (EoN) Node. + * + * @return the edge node ID */ public String getEdgeNodeId() { return edgeNodeId; } /** + * Returns the ID of the device. + * * @return the device ID */ public String getDeviceId() { return deviceId; } + public void updateDeviceIdPlus(String deviceIdNew) { + this.deviceId = this.deviceId.equals("+") ? deviceIdNew : this.deviceId; + } + + /** + * Returns the Host Application ID if this is a Host topic + * + * @return the Host Application ID + */ + public String getHostApplicationId() { + return hostApplicationId; + } + /** + * Returns the message type. + * * @return the message type */ public SparkplugMessageType getType() { @@ -140,12 +300,15 @@ public class SparkplugTopic { @Override public String toString() { - StringBuilder sb = new StringBuilder(getNamespace()).append("/") - .append(getGroupId()).append("/") - .append(getType()).append("/") - .append(getEdgeNodeId()); - if (getDeviceId() != null) { - sb.append("/").append(getDeviceId()); + StringBuilder sb = new StringBuilder(); + if (hostApplicationId == null) { + sb.append(getNamespace()).append("/").append(getGroupId()).append("/").append(getType()).append("/") + .append(getEdgeNodeId()); + if (getDeviceId() != null) { + sb.append("/").append(getDeviceId()); + } + } else { + sb.append(getNamespace()).append("/").append(getType()).append("/").append(hostApplicationId); } return sb.toString(); } @@ -165,5 +328,13 @@ public class SparkplugTopic { public String getNodeDeviceName() { return isNode() ? edgeNodeId : deviceId; } + + public static boolean isValidIdElementToUTF8(String deviceIdElement) { + if (deviceIdElement == null) { + return false; + } + String regex = "^(?!.*//)[^+#]*$"; + return deviceIdElement.matches(regex); + } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicService.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicService.java new file mode 100644 index 0000000000..52f1680a79 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicService.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2024 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.transport.mqtt.util.sparkplug; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.transport.mqtt.TbMqttTransportComponent; + +import java.util.HashMap; +import java.util.Map; + +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic.parseTopic; + +@Slf4j +@Service +@TbMqttTransportComponent +public class SparkplugTopicService { + + private static final Map SPLIT_TOPIC_CACHE = new HashMap<>(); + public static final String TOPIC_ROOT_SPB_V_1_0 = "spBv1.0"; + public static final String TOPIC_ROOT_CERT_SP = "$sparkplug/certificates/"; + public static final String TOPIC_SPLIT_REGEXP = "/"; + public static final String TOPIC_STATE_REGEXP = TOPIC_SPLIT_REGEXP + STATE.name() + TOPIC_SPLIT_REGEXP; + + public static SparkplugTopic getSplitTopic(String topic) throws ThingsboardException { + SparkplugTopic sparkplugTopic = SPLIT_TOPIC_CACHE.get(topic); + if (sparkplugTopic == null) { + // validation topic + sparkplugTopic = parseTopic(topic); + SPLIT_TOPIC_CACHE.put(topic, sparkplugTopic); + } + return sparkplugTopic; + } + + /** + * all ID Element MUST be a UTF-8 string + * and with the exception of the reserved characters of + (plus), / (forward slash). + * Publish: $sparkplug/certificates/spBv1.0/G1/NBIRTH/E1 + * Publish: spBv1.0/G1/NBIRTH/E1 + * Publish: $sparkplug/certificates/spBv1.0/G1/DBIRTH/E1/D1 + * Publish: spBv1.0/G1/DBIRTH/E1/D1 + * @param topic + * @return + * @throws ThingsboardException + */ + public static SparkplugTopic parseTopicPublish(String topic) throws ThingsboardException { + topic = topic.startsWith(TOPIC_ROOT_CERT_SP) ? topic.substring(TOPIC_ROOT_CERT_SP.length()) : topic; + topic = topic.indexOf("+") > 0 ? topic.substring(0, topic.indexOf("+")): topic; + return getSplitTopic(topic); + } +} + diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicUtil.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicUtil.java deleted file mode 100644 index 21d1547984..0000000000 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicUtil.java +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Copyright © 2016-2024 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.transport.mqtt.util.sparkplug; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; -import org.thingsboard.server.common.data.exception.ThingsboardException; - -import java.util.HashMap; -import java.util.Map; - -/** - * Provides utility methods for handling Sparkplug MQTT message topics. - */ -public class SparkplugTopicUtil { - - private static final Map SPLIT_TOPIC_CACHE = new HashMap(); - private static final String TOPIC_INVALID_NUMBER = "Invalid number of topic elements: "; - public static final String NAMESPACE = "spBv1.0"; - - public static String[] getSplitTopic(String topic) { - String[] splitTopic = SPLIT_TOPIC_CACHE.get(topic); - if (splitTopic == null) { - splitTopic = topic.split("/"); - SPLIT_TOPIC_CACHE.put(topic, splitTopic); - } - - return splitTopic; - } - - /** - * Serializes a {@link SparkplugTopic} instance in to a JSON string. - * - * @param topic a {@link SparkplugTopic} instance - * @return a JSON string - * @throws JsonProcessingException - */ - public static String sparkplugTopicToString(SparkplugTopic topic) throws JsonProcessingException { - ObjectMapper mapper = new ObjectMapper(); - return mapper.writeValueAsString(topic); - } - - /** - * Parses a Sparkplug MQTT message topic string and returns a {@link SparkplugTopic} instance. - * - * @param topic a topic string - * @return a {@link SparkplugTopic} instance - * @throws ThingsboardException if an error occurs while parsing - */ - public static SparkplugTopic parseTopicSubscribe(String topic) throws ThingsboardException { - // TODO "+", "$" - topic = topic.indexOf("#") > 0 ? topic.substring(0, topic.indexOf("#")) : topic; - return parseTopic(SparkplugTopicUtil.getSplitTopic(topic)); - } - - public static SparkplugTopic parseTopicPublish(String topic) throws ThingsboardException { - if (topic.contains("#") || topic.contains("$") || topic.contains("+")) { - throw new ThingsboardException("Invalid of topic elements for Publish", ThingsboardErrorCode.INVALID_ARGUMENTS); - } else { - String[] splitTopic = SparkplugTopicUtil.getSplitTopic(topic); - if (splitTopic.length < 4 || splitTopic.length > 5) { - throw new ThingsboardException(TOPIC_INVALID_NUMBER + splitTopic.length, ThingsboardErrorCode.INVALID_ARGUMENTS); - } - return parseTopic(splitTopic); - } - } - - /** - * Parses a Sparkplug MQTT message topic string and returns a {@link SparkplugTopic} instance. - * - * @param splitTopic a topic split into tokens - * @return a {@link SparkplugTopic} instance - * @throws Exception if an error occurs while parsing - */ - @SuppressWarnings("incomplete-switch") - public static SparkplugTopic parseTopic(String[] splitTopic) throws ThingsboardException { - int length = splitTopic.length; - if (length == 0) { - throw new ThingsboardException(TOPIC_INVALID_NUMBER + length, ThingsboardErrorCode.INVALID_ARGUMENTS); - } else { - SparkplugMessageType type; - String namespace, edgeNodeId, groupId, deviceId; - namespace = validateNameSpace(splitTopic[0]); - groupId = length > 1 ? splitTopic[1] : null; - type = length > 2 ? SparkplugMessageType.parseMessageType(splitTopic[2]) : null; - edgeNodeId = length > 3 ? splitTopic[3] : null; - deviceId = length > 4 ? splitTopic[4] : null; - return new SparkplugTopic(namespace, groupId, edgeNodeId, deviceId, type); - } - } - - /** - * For the Sparkplug™ B version of the specification, the UTF-8 string constant for the namespace element will be: "spBv1.0" - * @param nameSpace - * @return - */ - private static String validateNameSpace(String nameSpace) throws ThingsboardException { - if (NAMESPACE.equals(nameSpace)) return nameSpace; - throw new ThingsboardException("The namespace [" + nameSpace + "] is not valid and must be [" + NAMESPACE + "] for the Sparkplug™ B version.", ThingsboardErrorCode.INVALID_ARGUMENTS); - } - -} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SpecVersion.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SpecVersion.java new file mode 100644 index 0000000000..60bb73f72b --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SpecVersion.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2024 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.transport.mqtt.util.sparkplug; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +@Documented +@Target(ElementType.TYPE) +public @interface SpecVersion { + String spec() default ""; + String version(); +} \ No newline at end of file diff --git a/common/transport/mqtt/src/main/proto/sparkplug.proto b/common/transport/mqtt/src/main/proto/sparkplug.proto index f93561a7e8..55a529e429 100644 --- a/common/transport/mqtt/src/main/proto/sparkplug.proto +++ b/common/transport/mqtt/src/main/proto/sparkplug.proto @@ -21,51 +21,71 @@ import "google/protobuf/any.proto"; option java_package = "org.thingsboard.server.gen.transport.mqtt"; option java_outer_classname = "SparkplugBProto"; + +enum DataType { + // Indexes of Data Types + + // Unknown placeholder for future expansion. + Unknown = 0; + + // Basic Types + Int8 = 1; + Int16 = 2; + Int32 = 3; + Int64 = 4; + UInt8 = 5; + UInt16 = 6; + UInt32 = 7; + UInt64 = 8; + Float = 9; + Double = 10; + Boolean = 11; + String = 12; + DateTime = 13; + Text = 14; + + // Additional Metric Types + UUID = 15; + DataSet = 16; + Bytes = 17; + File = 18; + Template = 19; + + // Additional PropertyValue Types + PropertySet = 20; + PropertySetList = 21; + + // Array Types + Int8Array = 22; + Int16Array = 23; + Int32Array = 24; + Int64Array = 25; + UInt8Array = 26; + UInt16Array = 27; + UInt32Array = 28; + UInt64Array = 29; + FloatArray = 30; + DoubleArray = 31; + BooleanArray = 32; + StringArray = 33; + DateTimeArray = 34; +} + message Payload { - /* - // Indexes of Data Types - // Unknown placeholder for future expansion. - Unknown = 0; - // Basic Types - Int8 = 1; - Int16 = 2; - Int32 = 3; - Int64 = 4; - UInt8 = 5; - UInt16 = 6; - UInt32 = 7; - UInt64 = 8; - Float = 9; - Double = 10; - Boolean = 11; - String = 12; - DateTime = 13; - Text = 14; - // Additional Metric Types - UUID = 15; - DataSet = 16; - Bytes = 17; - File = 18; - Template = 19; - - // Additional PropertyValue Types - PropertySet = 20; - PropertySetList = 21; - */ message Template { message Parameter { - optional string name = 1; - optional uint32 type = 2; + optional string name = 1; + optional uint32 type = 2; oneof value { - uint32 int_value = 3; - uint64 long_value = 4; - float float_value = 5; - double double_value = 6; - bool boolean_value = 7; - string string_value = 8; + uint32 int_value = 3; + uint64 long_value = 4; + float float_value = 5; + double double_value = 6; + bool boolean_value = 7; + string string_value = 8; ParameterValueExtension extension_value = 9; } @@ -74,12 +94,12 @@ message Payload { } } - optional string version = 1; // The version of the Template to prevent mismatches - repeated Metric metrics = 2; // Each metric is the name of the metric and the datatype of the member but does not contain a value - repeated Parameter parameters = 3; - optional string template_ref = 4; // Reference to a template if this is extending a Template or an instance - must exist if an instance - optional bool is_definition = 5; - google.protobuf.Any extensions = 6; + optional string version = 1; // The version of the Template to prevent mismatches + repeated Metric metrics = 2; // Each metric is the name of the metric and the datatype of the member but does not contain a value + repeated Parameter parameters = 3; + optional string template_ref = 4; // Reference to a template if this is extending a Template or an instance - must exist if an instance + optional bool is_definition = 5; + google.protobuf.Any extensions = 6; } message DataSet { @@ -87,118 +107,118 @@ message Payload { message DataSetValue { oneof value { - uint32 int_value = 1; - uint64 long_value = 2; - float float_value = 3; - double double_value = 4; - bool boolean_value = 5; - string string_value = 6; - DataSetValueExtension extension_value = 7; + uint32 int_value = 1; + uint64 long_value = 2; + float float_value = 3; + double double_value = 4; + bool boolean_value = 5; + string string_value = 6; + DataSetValueExtension extension_value = 7; } message DataSetValueExtension { - google.protobuf.Any extensions = 1; + google.protobuf.Any extensions = 1; } } message Row { - repeated DataSetValue elements = 1; - google.protobuf.Any extensions = 2; // For third party extensions + repeated DataSetValue elements = 1; + google.protobuf.Any extensions = 2; // For third party extensions } - optional uint64 num_of_columns = 1; - repeated string columns = 2; - repeated uint32 types = 3; - repeated Row rows = 4; - google.protobuf.Any extensions = 5; // For third party extensions + optional uint64 num_of_columns = 1; + repeated string columns = 2; + repeated uint32 types = 3; + repeated Row rows = 4; + google.protobuf.Any extensions = 5; // For third party extensions } message PropertyValue { - optional uint32 type = 1; - optional bool is_null = 2; + optional uint32 type = 1; + optional bool is_null = 2; oneof value { - uint32 int_value = 3; - uint64 long_value = 4; - float float_value = 5; - double double_value = 6; - bool boolean_value = 7; - string string_value = 8; - PropertySet propertyset_value = 9; - PropertySetList propertysets_value = 10; // List of Property Values - PropertyValueExtension extension_value = 11; + uint32 int_value = 3; + uint64 long_value = 4; + float float_value = 5; + double double_value = 6; + bool boolean_value = 7; + string string_value = 8; + PropertySet propertyset_value = 9; + PropertySetList propertysets_value = 10; // List of Property Values + PropertyValueExtension extension_value = 11; } message PropertyValueExtension { - google.protobuf.Any extensions = 1; + google.protobuf.Any extensions = 1; } } message PropertySet { - repeated string keys = 1; // Names of the properties - repeated PropertyValue values = 2; - google.protobuf.Any extensions = 3; + repeated string keys = 1; // Names of the properties + repeated PropertyValue values = 2; + google.protobuf.Any extensions = 3; } message PropertySetList { repeated PropertySet propertyset = 1; - google.protobuf.Any extensions = 2; + google.protobuf.Any extensions = 2; } message MetaData { // Bytes specific metadata - optional bool is_multi_part = 1; + optional bool is_multi_part = 1; // General metadata - optional string content_type = 2; // Content/Media type - optional uint64 size = 3; // File size, String size, Multi-part size, etc - optional uint64 seq = 4; // Sequence number for multi-part messages + optional string content_type = 2; // Content/Media type + optional uint64 size = 3; // File size, String size, Multi-part size, etc + optional uint64 seq = 4; // Sequence number for multi-part messages // File metadata - optional string file_name = 5; // File name - optional string file_type = 6; // File type (i.e. xml, json, txt, cpp, etc) - optional string md5 = 7; // md5 of data + optional string file_name = 5; // File name + optional string file_type = 6; // File type (i.e. xml, json, txt, cpp, etc) + optional string md5 = 7; // md5 of data // Catchalls and future expansion - optional string description = 8; // Could be anything such as json or xml of custom properties - google.protobuf.Any extensions = 9; + optional string description = 8; // Could be anything such as json or xml of custom properties + google.protobuf.Any extensions = 9; } message Metric { - optional string name = 1; // Metric name - should only be included on birth - optional uint64 alias = 2; // Metric alias - tied to name on birth and included in all later DATA messages - optional uint64 timestamp = 3; // Timestamp associated with data acquisition time - optional uint32 datatype = 4; // DataType of the metric/tag value - optional bool is_historical = 5; // If this is historical data and should not update real time tag - optional bool is_transient = 6; // Tells consuming clients such as MQTT Engine to not store this as a tag - optional bool is_null = 7; // If this is null - explicitly say so rather than using -1, false, etc for some datatypes. - optional MetaData metadata = 8; // Metadata for the payload + optional string name = 1; // Metric name - should only be included on birth + optional uint64 alias = 2; // Metric alias - tied to name on birth and included in all later DATA messages + optional uint64 timestamp = 3; // Timestamp associated with data acquisition time + optional uint32 datatype = 4; // DataType of the metric/tag value + optional bool is_historical = 5; // If this is historical data and should not update real time tag + optional bool is_transient = 6; // Tells consuming clients such as MQTT Engine to not store this as a tag + optional bool is_null = 7; // If this is null - explicitly say so rather than using -1, false, etc for some datatypes. + optional MetaData metadata = 8; // Metadata for the payload optional PropertySet properties = 9; oneof value { - uint32 int_value = 10; - uint64 long_value = 11; - float float_value = 12; - double double_value = 13; - bool boolean_value = 14; - string string_value = 15; - bytes bytes_value = 16; // Bytes, File - DataSet dataset_value = 17; - Template template_value = 18; - MetricValueExtension extension_value = 19; + uint32 int_value = 10; + uint64 long_value = 11; + float float_value = 12; + double double_value = 13; + bool boolean_value = 14; + string string_value = 15; + bytes bytes_value = 16; // Bytes, File + DataSet dataset_value = 17; + Template template_value = 18; + MetricValueExtension extension_value = 19; } message MetricValueExtension { - google.protobuf.Any extensions = 1; + google.protobuf.Any extensions = 1; } } - optional uint64 timestamp = 1; // Timestamp at message sending time - repeated Metric metrics = 2; // Repeated forever - no limit in Google Protobufs - optional uint64 seq = 3; // Sequence number - optional string uuid = 4; // UUID to track message type in terms of schema definitions - optional bytes body = 5; // To optionally bypass the whole definition above - google.protobuf.Any extensions = 6; + optional uint64 timestamp = 1; // Timestamp at message sending time + repeated Metric metrics = 2; // Repeated forever - no limit in Google Protobufs + optional uint64 seq = 3; // Sequence number + optional string uuid = 4; // UUID to track message type in terms of schema definitions + optional bytes body = 5; // To optionally bypass the whole definition above + google.protobuf.Any extensions = 6; } \ No newline at end of file From b433367526c131df5b6c741b6f3c916832e0940d Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 23 Jan 2025 17:34:02 +0200 Subject: [PATCH 005/235] sparkplug: add TOPIC_STATE_REGEXP --- .../mqtt/session/SparkplugNodeSessionHandler.java | 11 ++++++----- .../mqtt/util/sparkplug/SparkplugTopicService.java | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java index 7af0487b49..2a3270ab5e 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java @@ -60,6 +60,7 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetr import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.fromSparkplugBMetricToKeyValueProto; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.validatedValueByTypeMetric; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_SPLIT_REGEXP; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_STATE_REGEXP; /** * Created by nickAS21 on 12.12.22 @@ -173,10 +174,9 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler= 4 && splitTopic.length <= 5 && splitTopic[0].equals(this.sparkplugTopicNode.getNamespace()) && splitTopic[1].equals(this.sparkplugTopicNode.getGroupId()) && diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicService.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicService.java index 52f1680a79..b9b3cd8e45 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicService.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicService.java @@ -35,7 +35,7 @@ public class SparkplugTopicService { public static final String TOPIC_ROOT_SPB_V_1_0 = "spBv1.0"; public static final String TOPIC_ROOT_CERT_SP = "$sparkplug/certificates/"; public static final String TOPIC_SPLIT_REGEXP = "/"; - public static final String TOPIC_STATE_REGEXP = TOPIC_SPLIT_REGEXP + STATE.name() + TOPIC_SPLIT_REGEXP; + public static final String TOPIC_STATE_REGEXP = TOPIC_ROOT_SPB_V_1_0 + TOPIC_SPLIT_REGEXP + STATE.name() + TOPIC_SPLIT_REGEXP; public static SparkplugTopic getSplitTopic(String topic) throws ThingsboardException { SparkplugTopic sparkplugTopic = SPLIT_TOPIC_CACHE.get(topic); From 9873b817e6100d499021f143e08ccfb594988327 Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 25 Feb 2025 15:53:26 +0200 Subject: [PATCH 006/235] sparkplug: fix bug alias --- .../AbstractMqttV5ClientSparkplugTest.java | 88 +++++++++++++++---- ...ctMqttV5ClientSparkplugConnectionTest.java | 2 +- .../rpc/AbstractMqttV5RpcSparkplugTest.java | 32 ++++++- .../sparkplug/rpc/MqttV5RpcSparkplugTest.java | 9 ++ .../SparkplugDeviceSessionContext.java | 16 +++- .../session/SparkplugNodeSessionHandler.java | 86 +++++++++++++----- .../util/sparkplug/SparkplugMetricUtil.java | 21 +++-- 7 files changed, 204 insertions(+), 50 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java index f10621842f..c512527066 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java @@ -29,6 +29,7 @@ import org.eclipse.paho.mqttv5.common.packet.MqttReturnCode; import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; import org.junit.Assert; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -112,21 +113,29 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte long value = bdSeq = 0; clientWithCorrectNodeAccessTokenWithNDEATH(ts, value); } + public void clientWithCorrectNodeAccessTokenWithNDEATH(Long alias) throws Exception { + long ts = calendar.getTimeInMillis(); + long value = bdSeq = 0; + clientMqttV5ConnectWithNDEATH(ts, value,alias); + } public void clientWithCorrectNodeAccessTokenWithNDEATH(long ts, long value) throws Exception { - IMqttToken connectionResult = clientMqttV5ConnectWithNDEATH(ts, value); + IMqttToken connectionResult = clientMqttV5ConnectWithNDEATH(ts, value, -1L); MqttWireMessage response = connectionResult.getResponse(); Assert.assertEquals(MESSAGE_TYPE_CONNACK, response.getType()); MqttConnAck connAckMsg = (MqttConnAck) response; Assert.assertEquals(MqttReturnCode.RETURN_CODE_SUCCESS, connAckMsg.getReturnCode()); } - public IMqttToken clientMqttV5ConnectWithNDEATH(long ts, long value, String... nameSpaceBad) throws Exception { - String key = keysBdSeq; + public IMqttToken clientMqttV5ConnectWithNDEATH(long ts, long value, Long alias, String... nameSpaceBad) throws Exception { + return clientMqttV5ConnectWithNDEATH(ts, value, null, alias, nameSpaceBad); + } + public IMqttToken clientMqttV5ConnectWithNDEATH(long ts, long value, String metricName, Long alias, String... nameSpaceBad) throws Exception { + String key = metricName == null ? keysBdSeq : metricName; MetricDataType metricDataType = Int64; SparkplugBProto.Payload.Builder deathPayload = SparkplugBProto.Payload.newBuilder() .setTimestamp(calendar.getTimeInMillis()); - deathPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + deathPayload.addMetrics(createMetric(value, ts, key, metricDataType, alias)); byte[] deathBytes = deathPayload.build().toByteArray(); this.client = new MqttV5TestClient(); this.mqttCallback = new SparkplugMqttCallback(); @@ -153,7 +162,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte MetricDataType metricDataType = Int32; String key = "Node Metric int32"; int valueDeviceInt32 = 1024; - SparkplugBProto.Payload.Metric metric = createMetric(valueDeviceInt32, ts, key, metricDataType); + SparkplugBProto.Payload.Metric metric = createMetric(valueDeviceInt32, ts, key, metricDataType, -1L); SparkplugBProto.Payload.Builder payloadBirthNode = SparkplugBProto.Payload.newBuilder() .setTimestamp(ts) .setSeq(getBdSeqNum()); @@ -165,7 +174,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte } valueDeviceInt32 = 4024; - metric = createMetric(valueDeviceInt32, ts, metricBirthName_Int32, metricBirthDataType_Int32); + metric = createMetric(valueDeviceInt32, ts, metricBirthName_Int32, metricBirthDataType_Int32, -1L); for (int i = 0; i < cntDevices; i++) { SparkplugBProto.Payload.Builder payloadBirthDevice = SparkplugBProto.Payload.newBuilder() .setTimestamp(ts) @@ -192,6 +201,49 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte return devices; } + protected List connectClientWithCorrectAccessTokenWithNDEATHWithAliasCreatedDevices(long ts) throws Exception { + List devices = new ArrayList<>(); + Long alias = 0L; + clientWithCorrectNodeAccessTokenWithNDEATH(alias++); + MetricDataType metricDataType = Int32; + String key = "Node Metric int32"; + int valueDeviceInt32 = 1024; + SparkplugBProto.Payload.Metric metric = createMetric(valueDeviceInt32, ts, key, metricDataType, alias++); + SparkplugBProto.Payload.Builder payloadBirthNode = SparkplugBProto.Payload.newBuilder() + .setTimestamp(ts) + .setSeq(getBdSeqNum()); + payloadBirthNode.addMetrics(metric); + payloadBirthNode.setTimestamp(ts); + if (client.isConnected()) { + client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode, + payloadBirthNode.build().toByteArray(), 0, false); + } + + valueDeviceInt32 = 4024; + metric = createMetric(valueDeviceInt32, ts, metricBirthName_Int32, metricBirthDataType_Int32, alias++); + SparkplugBProto.Payload.Builder payloadBirthDevice = SparkplugBProto.Payload.newBuilder() + .setTimestamp(ts) + .setSeq(getSeqNum()); + String deviceName = deviceId + "_" + 1; + + payloadBirthDevice.addMetrics(metric); + if (client.isConnected()) { + client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.DBIRTH.name() + "/" + edgeNode + "/" + deviceName, + payloadBirthDevice.build().toByteArray(), 0, false); + AtomicReference device = new AtomicReference<>(); + await(alias + "find device [" + deviceName + "] after created") + .atMost(200, TimeUnit.SECONDS) + .until(() -> { + device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class)); + return device.get() != null; + }); + devices.add(device.get()); + } + + Assert.assertEquals(1, devices.size()); + return devices; + } + protected long getBdSeqNum() throws Exception { if (bdSeq == 256) { bdSeq = 0; @@ -212,12 +264,16 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte .setTimestamp(calendar.getTimeInMillis()); long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; long valueBdSec = getBdSeqNum(); - payloadBirthNode.addMetrics(createMetric(valueBdSec, ts, keysBdSeq, Int64)); + payloadBirthNode.addMetrics(createMetric(valueBdSec, ts, keysBdSeq, Int64, -1L)); listKeys.add(SparkplugMessageType.NBIRTH.name() + " " + keysBdSeq); - payloadBirthNode.addMetrics(createMetric(false, ts, keyNodeRebirth, MetricDataType.Boolean)); + payloadBirthNode.addMetrics(createMetric(false, ts, keyNodeRebirth, MetricDataType.Boolean, -1L)); listKeys.add(keyNodeRebirth); - payloadBirthNode.addMetrics(createMetric(metricValue, ts, metricKey, metricDataType)); + if (StringUtils.isNotBlank(metricKey)) { + payloadBirthNode.addMetrics(createMetric(metricValue, ts, metricKey, metricDataType, -1L)); + } else { + payloadBirthNode.addMetrics(createMetric(metricValue, ts, metricKey, metricDataType, 4L)); + } listKeys.add(metricKey); if (client.isConnected()) { @@ -302,7 +358,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte private TsKvEntry createdAddMetricTsKvLong(SparkplugBProto.Payload.Builder dataPayload, String key, Object value, long ts, MetricDataType metricDataType) throws ThingsboardException { TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(key, Long.valueOf(String.valueOf(value)))); - dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + dataPayload.addMetrics(createMetric(value, ts, key, metricDataType, -1L)); return tsKvEntry; } @@ -310,7 +366,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte long ts, MetricDataType metricDataType) throws ThingsboardException { Double dd = Double.parseDouble(Float.toString(value)); TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new DoubleDataEntry(key, dd)); - dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + dataPayload.addMetrics(createMetric(value, ts, key, metricDataType, -1L)); return tsKvEntry; } @@ -318,21 +374,21 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte long ts, MetricDataType metricDataType) throws ThingsboardException { Long l = Double.valueOf(value).longValue(); TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(key, l)); - dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + dataPayload.addMetrics(createMetric(value, ts, key, metricDataType, -1L)); return tsKvEntry; } private TsKvEntry createdAddMetricTsKvBoolean(SparkplugBProto.Payload.Builder dataPayload, String key, boolean value, long ts, MetricDataType metricDataType) throws ThingsboardException { TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new BooleanDataEntry(key, value)); - dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + dataPayload.addMetrics(createMetric(value, ts, key, metricDataType, -1L)); return tsKvEntry; } private TsKvEntry createdAddMetricTsKvString(SparkplugBProto.Payload.Builder dataPayload, String key, String value, long ts, MetricDataType metricDataType) throws ThingsboardException { TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(key, value)); - dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + dataPayload.addMetrics(createMetric(value, ts, key, metricDataType, -1L)); return tsKvEntry; } @@ -353,7 +409,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte if (nodeArray.size() > 0) { Optional tsKvEntryOptional = Optional.of(new BasicTsKvEntry(ts, new JsonDataEntry(key, nodeArray.toString()))); if (tsKvEntryOptional.isPresent()) { - dataPayload.addMetrics(createMetric(values, ts, key, metricDataType)); + dataPayload.addMetrics(createMetric(values, ts, key, metricDataType, -1L)); listTsKvEntry.add(tsKvEntryOptional.get()); listKeys.add(key); } @@ -421,7 +477,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte return java.util.UUID.randomUUID().toString(); } - public class SparkplugMqttCallback implements MqttCallback { + public class SparkplugMqttCallback implements MqttCallback { private final List messageArrivedMetrics = new ArrayList<>(); @Override diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java index dee295604c..941b7c0c69 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java @@ -80,7 +80,7 @@ public abstract class AbstractMqttV5ClientSparkplugConnectionTest extends Abstra protected void processClientWithCorrectNodeAccessTokenNameSpaceInvalid_Test() throws Exception { long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; long value = bdSeq = 0; - MqttException actualException = Assert.assertThrows(MqttException.class, () -> clientMqttV5ConnectWithNDEATH(ts, value, "spBv1.2")); + MqttException actualException = Assert.assertThrows(MqttException.class, () -> clientMqttV5ConnectWithNDEATH(ts, value, -1L,"spBv1.2")); String expectedMessage = "Server unavailable."; int expectedReasonCode = 136; Assert.assertEquals(expectedMessage, actualException.getMessage()); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java index 4bec661c7d..47a07abdbe 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java @@ -15,8 +15,8 @@ */ package org.thingsboard.server.transport.mqtt.sparkplug.rpc; -import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.mqttv5.common.MqttException; import org.junit.Assert; import org.junit.Test; import org.thingsboard.server.common.data.Device; @@ -74,6 +74,36 @@ public abstract class AbstractMqttV5RpcSparkplugTest extends AbstractMqttV5Clie Assert.assertTrue(metricBirthValue_Int32 == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); } + @Test + public void processClientDeviceWithCorrectAccessTokenPublishWithAlias_TwoWayRpc_Success() throws Exception { + long ts = calendar.getTimeInMillis(); + List devices = connectClientWithCorrectAccessTokenWithNDEATHWithAliasCreatedDevices(ts); + awaitForDeviceActorToReceiveSubscription(devices.get(0).getId(), FeatureType.RPC, 1); + String expected = "{\"result\":\"Success: " + DCMD.name() + "\"}"; + String actual = sendRPCSparkplug(DCMD.name() , sparkplugRpcRequest, devices.get(0)); + await(alias + DCMD.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(expected, actual); + Assert.assertFalse(mqttCallback.getMessageArrivedMetrics().get(0).hasName()); + Assert.assertTrue(mqttCallback.getMessageArrivedMetrics().get(0).hasAlias()); + Assert.assertTrue(2L == mqttCallback.getMessageArrivedMetrics().get(0).getAlias()); + Assert.assertTrue(metricBirthValue_Int32 == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); + } + + @Test + public void processClientNodeWithCorrectAccessTokenPublishWithAliasWithoutMetricName_TwoWayRpc_BAD_REQUEST_PARAMS() throws Exception { + long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; + long value = bdSeq = 0; + MqttException actualException = Assert.assertThrows(MqttException.class, () -> clientMqttV5ConnectWithNDEATH(ts, value, "",4L)); + String expectedMessage = "Server unavailable."; + int expectedReasonCode = 136; + Assert.assertEquals(expectedMessage, actualException.getMessage()); + Assert.assertEquals(expectedReasonCode, actualException.getReasonCode()); + } + @Test public void processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS() throws Exception { clientWithCorrectNodeAccessTokenWithNDEATH(); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java index fec60ad122..0c97bca36b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java @@ -47,6 +47,15 @@ public class MqttV5RpcSparkplugTest extends AbstractMqttV5RpcSparkplugTest { public void testClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception { processClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success(); } + @Test + public void testClientDeviceWithCorrectAccessTokenPublishWithAlias_TwoWayRpc_Success() throws Exception { + processClientDeviceWithCorrectAccessTokenPublishWithAlias_TwoWayRpc_Success(); + } + + @Test + public void testClientNodeWithCorrectAccessTokenPublishWithAliasWithoutMetricName_TwoWayRpc_BAD_REQUEST_PARAMS() throws Exception { + processClientNodeWithCorrectAccessTokenPublishWithAliasWithoutMetricName_TwoWayRpc_BAD_REQUEST_PARAMS(); + } @Test public void testClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS() throws Exception { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugDeviceSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugDeviceSessionContext.java index ccbb0e19b4..e854402815 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugDeviceSessionContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugDeviceSessionContext.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.mqtt.session; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DuplicateKeyException; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @@ -33,7 +34,6 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.stream.Collectors; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DCMD; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.getTsKvProto; @@ -57,9 +57,17 @@ public class SparkplugDeviceSessionContext extends AbstractGatewayDeviceSessionC return deviceBirthMetrics; } - public void setDeviceBirthMetrics(java.util.List metrics) { - this.deviceBirthMetrics.putAll(metrics.stream() - .collect(Collectors.toMap(SparkplugBProto.Payload.Metric::getName, metric -> metric))); + public void setDeviceBirthMetrics(java.util.List metrics) { + for (var metric : metrics) { + if (metric.hasName()) { + this.deviceBirthMetrics.put(metric.getName(), metric); + } else { + throw new IllegalArgumentException("The metric name of device: '" + this.getDeviceInfo().getDeviceName() + "' must not be empty or null! Metric: [" + metric + "]"); + } + if (metric.hasAlias() && this.parent.getNodeAlias().putIfAbsent(metric.getAlias(), metric.getName()) != null) { + throw new DuplicateKeyException("The alias '" + metric.getAlias() + "' already exists in device: '" + this.getDeviceInfo().getDeviceName() + "'"); + } + } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java index 2a3270ab5e..434d4ec486 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java @@ -24,6 +24,7 @@ import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttTopicSubscription; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DuplicateKeyException; import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.common.adaptor.ProtoConverter; @@ -47,12 +48,10 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.ONLINE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DBIRTH; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NBIRTH; -import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.parseMessageType; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.SPARKPLUG_BD_SEQUENCE_NUMBER_KEY; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.SPARKPLUG_SEQUENCE_NUMBER_KEY; @@ -73,6 +72,8 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler nodeBirthMetrics; + @Getter + private final Map nodeAlias; private final MqttTransportHandler parent; public SparkplugNodeSessionHandler(MqttTransportHandler parent, DeviceSessionCtx deviceSessionCtx, UUID sessionId, @@ -81,11 +82,20 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler(); + this.nodeAlias = new ConcurrentHashMap<>(); } - public void setNodeBirthMetrics(java.util.List metrics) { - this.nodeBirthMetrics.putAll(metrics.stream() - .collect(Collectors.toMap(SparkplugBProto.Payload.Metric::getName, metric -> metric))); + public void setNodeBirthMetrics(java.util.List metrics) throws AdaptorException { + for (var metric : metrics) { + if (metric.hasName()) { + this.nodeBirthMetrics.put(metric.getName(), metric); + } else { + throw new AdaptorException("The metric name of edgeNode: '" + this.sparkplugTopicNode.getEdgeNodeId() + "' must not be empty or null! Metric: [" + metric + "]"); + } + if (metric.hasAlias() && this.nodeAlias.putIfAbsent(metric.getAlias(), metric.getName()) != null) { + throw new AdaptorException("The alias '" + metric.getAlias() + "' already exists in edgeNode: '" + this.sparkplugTopicNode.getEdgeNodeId() + "'"); + } + } } @@ -113,7 +123,11 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler 6.4.6. Metric + * https://sparkplug.eclipse.org/specification/version/3.0/documents/sparkplug-specification-3.0.0.pdf#%5B%7B%22num%22%3A339%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C0%2C455.52%2Cnull%5D + * [tck-id-payloads-name-requirement] The name MUST be included with every metric unless aliases are being used. ◦ All UTF-8 characters are allowed in the metric name. However, special characters including but not limited to the following are discouraged: . , \ @ # $ % ^ & * ( ) [ ] { } | ! ` ~ : ; ' " < > ?. This is because many Sparkplug Host Applications may have issues handling them. + * • alias (are optional and not required): + * - This is an unsigned 64-bit integer representing an optional alias for a Sparkplug B payload. + * - If aliases are used, the following rules apply: + * -- [tck-id-payloads-alias-uniqueness] If supplied in an NBIRTH or BIRTH it MUST be a unique number across this Edge Node’s entire set of metrics. + * -- no two metrics for the same Edge Node can have the same alias. + * -- [tck-id-payloads-alias-birth-requirement] NBIRTH and DBIRTH messages MUST include both a metric name and alias. + * -- [tck-id-payloads-alias-data-cmd-requirement] NDATA, DDATA, NCMD, and DCMD messages MUST only include an alias and the metric name MUST be excluded. + * @param sparkplugBProto + * @param attributesMetricNames + * @param topicTypeName + * @return + * @throws AdaptorException + */ private List convertToPostTelemetry(SparkplugBProto.Payload sparkplugBProto, Set attributesMetricNames, String topicTypeName) throws AdaptorException { try { List msgs = new ArrayList<>(); for (SparkplugBProto.Payload.Metric protoMetric : sparkplugBProto.getMetricsList()) { - if (attributesMetricNames == null || !matches(attributesMetricNames, protoMetric)) { - long ts = protoMetric.getTimestamp(); - String key = SPARKPLUG_BD_SEQUENCE_NUMBER_KEY.equals(protoMetric.getName()) ? - topicTypeName + " " + protoMetric.getName() : protoMetric.getName(); - Optional keyValueProtoOpt = fromSparkplugBMetricToKeyValueProto(key, protoMetric); - keyValueProtoOpt.ifPresent(kvProto -> msgs.add(postTelemetryMsgCreated(kvProto, ts))); + String metricName = protoMetric.hasName() ? protoMetric.getName() : protoMetric.hasAlias() ? this.nodeAlias.get(protoMetric.getAlias()) : null; + if (metricName == null) { + throw new ThingsboardException("Metric without metricName and alias.", ThingsboardErrorCode.INVALID_ARGUMENTS); + } else { + if (attributesMetricNames == null || !matches(attributesMetricNames, metricName)) { + long ts = protoMetric.getTimestamp(); + String key = SPARKPLUG_BD_SEQUENCE_NUMBER_KEY.equals(protoMetric.getName()) ? + topicTypeName + " " + protoMetric.getName() : protoMetric.getName(); + Optional keyValueProtoOpt = fromSparkplugBMetricToKeyValueProto(key, protoMetric); + keyValueProtoOpt.ifPresent(kvProto -> msgs.add(postTelemetryMsgCreated(kvProto, ts))); + } } } @@ -237,13 +273,18 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler msgs = new ArrayList<>(); for (SparkplugBProto.Payload.Metric protoMetric : sparkplugBProto.getMetricsList()) { - if (matches(attributesMetricNames, protoMetric)) { - TransportApiProtos.AttributesMsg.Builder deviceAttributesMsgBuilder = TransportApiProtos.AttributesMsg.newBuilder(); - Optional msgOpt = getPostAttributeMsg(protoMetric); - if (msgOpt.isPresent()) { - deviceAttributesMsgBuilder.setDeviceName(deviceName); - deviceAttributesMsgBuilder.setMsg(msgOpt.get()); - msgs.add(deviceAttributesMsgBuilder.build()); + String metricName = protoMetric.hasName() ? protoMetric.getName() : protoMetric.hasAlias() ? this.nodeAlias.get(protoMetric.getAlias()) : null; + if (metricName == null) { + throw new ThingsboardException("Metric without metricName and alias.", ThingsboardErrorCode.INVALID_ARGUMENTS); + } else { + if (matches(attributesMetricNames, metricName)) { + TransportApiProtos.AttributesMsg.Builder deviceAttributesMsgBuilder = TransportApiProtos.AttributesMsg.newBuilder(); + Optional msgOpt = getPostAttributeMsg(protoMetric); + if (msgOpt.isPresent()) { + deviceAttributesMsgBuilder.setDeviceName(deviceName); + deviceAttributesMsgBuilder.setMsg(msgOpt.get()); + msgs.add(deviceAttributesMsgBuilder.build()); + } } } } @@ -254,8 +295,7 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler attributesMetricNames, SparkplugBProto.Payload.Metric protoMetric) { - String metricName = protoMetric.getName(); + private boolean matches(Set attributesMetricNames, String metricName) { for (String attributeMetricFilter : attributesMetricNames) { if (metricName.equals(attributeMetricFilter) || (attributeMetricFilter.endsWith("*") && metricName.startsWith( @@ -288,7 +328,9 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler= 0) { + metric.setAlias(alias); + } + if (StringUtils.isNotBlank(key)) { + metric.setName(key); + } + return addToMetricValue(value, metric.build(), metricDataType); + } - public static SparkplugBProto.Payload.Metric createMetric(Object value, long ts, String key, MetricDataType metricDataType) throws ThingsboardException { - SparkplugBProto.Payload.Metric metric = SparkplugBProto.Payload.Metric.newBuilder() - .setTimestamp(ts) - .setName(key) - .setDatatype(metricDataType.toIntValue()) - .build(); + public static SparkplugBProto.Payload.Metric addToMetricValue(Object value, SparkplugBProto.Payload.Metric metric, MetricDataType metricDataType) throws ThingsboardException { switch (metricDataType) { case Int8: // (byte) return metric.toBuilder().setIntValue(((Byte) value).intValue()).build(); From eba25ed79fabb6f6c4fd37eab38d6a363cdf1fc5 Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 27 Feb 2025 12:01:19 +0200 Subject: [PATCH 007/235] sparkplug: fix bug alias - license --- .../server/transport/mqtt/util/sparkplug/DeviceDescriptor.java | 2 +- .../transport/mqtt/util/sparkplug/EdgeNodeDescriptor.java | 2 +- .../transport/mqtt/util/sparkplug/SparkplugDescriptor.java | 2 +- .../transport/mqtt/util/sparkplug/SparkplugTopicService.java | 2 +- .../transport/mqtt/util/sparkplug/SparkplugTopicUtil.java | 0 .../server/transport/mqtt/util/sparkplug/SpecVersion.java | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicUtil.java diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/DeviceDescriptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/DeviceDescriptor.java index 02d994ae5e..cf684884f3 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/DeviceDescriptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/DeviceDescriptor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2024 The Thingsboard Authors + * 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. diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/EdgeNodeDescriptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/EdgeNodeDescriptor.java index 250e78ac0e..c8ea2c457e 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/EdgeNodeDescriptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/EdgeNodeDescriptor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2024 The Thingsboard Authors + * 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. diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugDescriptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugDescriptor.java index 7f84f4bd1d..9deff97bf4 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugDescriptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugDescriptor.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2024 The Thingsboard Authors + * 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. diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicService.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicService.java index b9b3cd8e45..a6e2e34553 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicService.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicService.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2024 The Thingsboard Authors + * 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. diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicUtil.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicUtil.java deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SpecVersion.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SpecVersion.java index 60bb73f72b..07f5cbf53f 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SpecVersion.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SpecVersion.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2024 The Thingsboard Authors + * 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. From 8bb4e9dc0b0e5ac3567c16c983c2eedef39bc23e Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 16 Apr 2025 18:31:11 +0200 Subject: [PATCH 008/235] 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 009/235] 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 010/235] 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 011/235] 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 012/235] 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 013/235] 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 014/235] 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 18c75998efc3903190c33680c411efca73b84a27 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 16 May 2025 15:21:17 +0300 Subject: [PATCH 015/235] 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 016/235] 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 017/235] 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 018/235] 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 019/235] 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 020/235] 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 021/235] 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 022/235] 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 023/235] 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 024/235] 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 025/235] 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 026/235] 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 027/235] 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 028/235] 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 029/235] 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 030/235] 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 031/235] 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 032/235] 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 033/235] 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 034/235] 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 035/235] 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 036/235] 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 a4db17c859797d8c02b8a29d324d6979483869c3 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 2 Jun 2025 16:28:53 +0300 Subject: [PATCH 037/235] 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 2de733ef69dc7fe391e4b3573679ec7e41e4df27 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 3 Jun 2025 18:42:01 +0300 Subject: [PATCH 038/235] Version set to 4.2.0 --- application/pom.xml | 2 +- common/actor/pom.xml | 2 +- common/cache/pom.xml | 2 +- common/cluster-api/pom.xml | 2 +- common/coap-server/pom.xml | 2 +- common/dao-api/pom.xml | 2 +- common/data/pom.xml | 2 +- common/discovery-api/pom.xml | 2 +- common/edge-api/pom.xml | 2 +- common/edqs/pom.xml | 2 +- common/message/pom.xml | 2 +- common/pom.xml | 2 +- common/proto/pom.xml | 2 +- common/queue/pom.xml | 2 +- common/script/pom.xml | 2 +- common/script/remote-js-client/pom.xml | 2 +- common/script/script-api/pom.xml | 2 +- common/stats/pom.xml | 2 +- common/transport/coap/pom.xml | 2 +- common/transport/http/pom.xml | 2 +- common/transport/lwm2m/pom.xml | 2 +- common/transport/mqtt/pom.xml | 2 +- common/transport/pom.xml | 2 +- common/transport/snmp/pom.xml | 2 +- common/transport/transport-api/pom.xml | 2 +- common/util/pom.xml | 2 +- common/version-control/pom.xml | 2 +- dao/pom.xml | 2 +- edqs/pom.xml | 2 +- monitoring/pom.xml | 2 +- msa/black-box-tests/pom.xml | 2 +- msa/edqs/pom.xml | 2 +- msa/js-executor/package.json | 2 +- msa/js-executor/pom.xml | 2 +- msa/monitoring/pom.xml | 2 +- msa/pom.xml | 2 +- msa/tb-node/pom.xml | 2 +- msa/tb/pom.xml | 2 +- msa/transport/coap/pom.xml | 2 +- msa/transport/http/pom.xml | 2 +- msa/transport/lwm2m/pom.xml | 2 +- msa/transport/mqtt/pom.xml | 2 +- msa/transport/pom.xml | 2 +- msa/transport/snmp/pom.xml | 2 +- msa/vc-executor-docker/pom.xml | 2 +- msa/vc-executor/pom.xml | 2 +- msa/web-ui/package.json | 2 +- msa/web-ui/pom.xml | 2 +- netty-mqtt/pom.xml | 4 ++-- pom.xml | 2 +- rest-client/pom.xml | 2 +- rule-engine/pom.xml | 2 +- rule-engine/rule-engine-api/pom.xml | 2 +- rule-engine/rule-engine-components/pom.xml | 2 +- tools/pom.xml | 2 +- transport/coap/pom.xml | 2 +- transport/http/pom.xml | 2 +- transport/lwm2m/pom.xml | 2 +- transport/mqtt/pom.xml | 2 +- transport/pom.xml | 2 +- transport/snmp/pom.xml | 2 +- ui-ngx/package.json | 2 +- ui-ngx/pom.xml | 2 +- 63 files changed, 64 insertions(+), 64 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index d775942f8a..ee66296dd0 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 thingsboard application diff --git a/common/actor/pom.xml b/common/actor/pom.xml index 084ef734fa..63baab7044 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 common org.thingsboard.common diff --git a/common/cache/pom.xml b/common/cache/pom.xml index 88eadb17cc..498782bc35 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 common org.thingsboard.common diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index c455dfa337..41dcd05d01 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 common org.thingsboard.common diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index 99ad0f0b56..7cb3adcab7 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 common org.thingsboard.common diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index cbf928ac73..5b63866b51 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 common org.thingsboard.common diff --git a/common/data/pom.xml b/common/data/pom.xml index fd0c38c3e7..d93d2ffc48 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 common org.thingsboard.common diff --git a/common/discovery-api/pom.xml b/common/discovery-api/pom.xml index 8d03a58bdf..d3a2012a79 100644 --- a/common/discovery-api/pom.xml +++ b/common/discovery-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 common org.thingsboard.common diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index 9185c7dc7b..7c5a3221b7 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 common org.thingsboard.common diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml index 4cb38ab7fe..c3f6c45294 100644 --- a/common/edqs/pom.xml +++ b/common/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 common org.thingsboard.common diff --git a/common/message/pom.xml b/common/message/pom.xml index 8e6a247ae9..2f7247d6bb 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 common org.thingsboard.common diff --git a/common/pom.xml b/common/pom.xml index 2e277f095f..c8e4e6e0ed 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 thingsboard common diff --git a/common/proto/pom.xml b/common/proto/pom.xml index 75a71c434d..b5dc55d976 100644 --- a/common/proto/pom.xml +++ b/common/proto/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 common org.thingsboard.common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index 0bed40051b..cb6de43d08 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 common org.thingsboard.common diff --git a/common/script/pom.xml b/common/script/pom.xml index 0a7448d837..e0473ca351 100644 --- a/common/script/pom.xml +++ b/common/script/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 common org.thingsboard.common diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml index b14cd0ef45..9a4d582418 100644 --- a/common/script/remote-js-client/pom.xml +++ b/common/script/remote-js-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.1.0-RC + 4.2.0 script org.thingsboard.common.script diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index d64fd6de4c..3c287cc8a8 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.1.0-RC + 4.2.0 script org.thingsboard.common.script diff --git a/common/stats/pom.xml b/common/stats/pom.xml index 0c425d5e2b..58872d35b8 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index ac0a036f1f..e64e4e49c2 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.1.0-RC + 4.2.0 transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index a2cdbfc0dc..73f27ff23d 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.1.0-RC + 4.2.0 transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 74955a8586..56f21148fe 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.1.0-RC + 4.2.0 transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 0f19e9dc29..71694edbc8 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.1.0-RC + 4.2.0 transport org.thingsboard.common.transport diff --git a/common/transport/pom.xml b/common/transport/pom.xml index 25190cf24a..5fa05d254f 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index 79e4387744..7976642d0e 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 4.1.0-RC + 4.2.0 transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 181b958649..e4f6208437 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.1.0-RC + 4.2.0 transport org.thingsboard.common.transport diff --git a/common/util/pom.xml b/common/util/pom.xml index 200eaed579..4da4ce36c3 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 common org.thingsboard.common diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml index 32c7210d0e..951f03b2b4 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 common org.thingsboard.common diff --git a/dao/pom.xml b/dao/pom.xml index 6680fc0d1e..473e81c447 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 thingsboard dao diff --git a/edqs/pom.xml b/edqs/pom.xml index ada51eff8e..5b1e6607d3 100644 --- a/edqs/pom.xml +++ b/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 thingsboard edqs diff --git a/monitoring/pom.xml b/monitoring/pom.xml index 15708fbdbf..3a24077afa 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -21,7 +21,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 thingsboard diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index 5890564a1f..e7e6a2d9e5 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.1.0-RC + 4.2.0 msa org.thingsboard.msa diff --git a/msa/edqs/pom.xml b/msa/edqs/pom.xml index 3c63c44d32..81382199c6 100644 --- a/msa/edqs/pom.xml +++ b/msa/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 msa org.thingsboard.msa diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index 1c16eaa594..4d417081d8 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "4.1.0", + "version": "4.2.0", "description": "ThingsBoard JavaScript Executor Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index 4cdb487968..bcd4f01d8a 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 msa org.thingsboard.msa diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml index 02390df0e8..7a1c99efc0 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 msa diff --git a/msa/pom.xml b/msa/pom.xml index 97bbed14b6..9961214a7b 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index d81143b3d9..35b34617df 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 msa org.thingsboard.msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 03481f16aa..0a6eb11e28 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index 0e0aa13d76..792d215793 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.1.0-RC + 4.2.0 transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index 81a0c40519..6f39a19944 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.1.0-RC + 4.2.0 transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index 7fa1d161bf..ef10b2da54 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.1.0-RC + 4.2.0 transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 9c70a376c8..068ba3b193 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.1.0-RC + 4.2.0 transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index ae99b7b515..1e31c86f06 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index 73c9a320f0..ad27d9f3b6 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 4.1.0-RC + 4.2.0 org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index 0aab19bfa1..dfcbae8856 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index 3a59886d9b..1bda1fc434 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.1.0-RC + 4.2.0 msa org.thingsboard.msa diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index 8df71fc347..de0c04fc9d 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-web-ui", "private": true, - "version": "4.1.0", + "version": "4.2.0", "description": "ThingsBoard Web UI Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index fc244b80dd..e5001c5c64 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index fc07fa42fe..3cba6f359f 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 thingsboard netty-mqtt - 4.1.0-RC + 4.2.0 jar Netty MQTT Client diff --git a/pom.xml b/pom.xml index 02f49f0677..681a7fd69a 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 4.1.0-RC + 4.2.0 pom Thingsboard diff --git a/rest-client/pom.xml b/rest-client/pom.xml index a7ed36b546..3ed87b9fab 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 thingsboard rest-client diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index 4213d70558..eb9c35846d 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 6c1aead1de..f5b60bc653 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index b4e74885d9..56fe5628ce 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 rule-engine org.thingsboard.rule-engine diff --git a/tools/pom.xml b/tools/pom.xml index 832b6e00ad..3e81be82a1 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index 050e287998..60b576aadf 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 transport org.thingsboard.transport diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 2cc3d25695..31bf0e6ebb 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 transport org.thingsboard.transport diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index 0518241889..0beac3a450 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 transport org.thingsboard.transport diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index a1b1cc455b..33a0bec8cc 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 transport org.thingsboard.transport diff --git a/transport/pom.xml b/transport/pom.xml index e0c638c995..6f93a832ac 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index 42f9bc92d2..1af7fe6acf 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.1.0-RC + 4.2.0 transport diff --git a/ui-ngx/package.json b/ui-ngx/package.json index e1cc3a90af..9514f5bfaf 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -1,6 +1,6 @@ { "name": "thingsboard", - "version": "4.1.0", + "version": "4.2.0", "scripts": { "ng": "ng", "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open", diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index 7fe99a56fa..beaa2314ba 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.1.0-RC + 4.2.0 thingsboard org.thingsboard From ebf6f66cbe50ab0a48b79317bfcbceb89585de5a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 3 Jun 2025 18:55:18 +0300 Subject: [PATCH 039/235] Version set to 4.2.0-SNAPSHOT --- application/pom.xml | 2 +- common/actor/pom.xml | 2 +- common/cache/pom.xml | 2 +- common/cluster-api/pom.xml | 2 +- common/coap-server/pom.xml | 2 +- common/dao-api/pom.xml | 2 +- common/data/pom.xml | 2 +- common/discovery-api/pom.xml | 2 +- common/edge-api/pom.xml | 2 +- common/edqs/pom.xml | 2 +- common/message/pom.xml | 2 +- common/pom.xml | 2 +- common/proto/pom.xml | 2 +- common/queue/pom.xml | 2 +- common/script/pom.xml | 2 +- common/script/remote-js-client/pom.xml | 2 +- common/script/script-api/pom.xml | 2 +- common/stats/pom.xml | 2 +- common/transport/coap/pom.xml | 2 +- common/transport/http/pom.xml | 2 +- common/transport/lwm2m/pom.xml | 2 +- common/transport/mqtt/pom.xml | 2 +- common/transport/pom.xml | 2 +- common/transport/snmp/pom.xml | 2 +- common/transport/transport-api/pom.xml | 2 +- common/util/pom.xml | 2 +- common/version-control/pom.xml | 2 +- dao/pom.xml | 2 +- edqs/pom.xml | 2 +- monitoring/pom.xml | 2 +- msa/black-box-tests/pom.xml | 2 +- msa/edqs/pom.xml | 2 +- msa/js-executor/pom.xml | 2 +- msa/monitoring/pom.xml | 2 +- msa/pom.xml | 2 +- msa/tb-node/pom.xml | 2 +- msa/tb/pom.xml | 2 +- msa/transport/coap/pom.xml | 2 +- msa/transport/http/pom.xml | 2 +- msa/transport/lwm2m/pom.xml | 2 +- msa/transport/mqtt/pom.xml | 2 +- msa/transport/pom.xml | 2 +- msa/transport/snmp/pom.xml | 2 +- msa/vc-executor-docker/pom.xml | 2 +- msa/vc-executor/pom.xml | 2 +- msa/web-ui/pom.xml | 2 +- netty-mqtt/pom.xml | 4 ++-- pom.xml | 2 +- rest-client/pom.xml | 2 +- rule-engine/pom.xml | 2 +- rule-engine/rule-engine-api/pom.xml | 2 +- rule-engine/rule-engine-components/pom.xml | 2 +- tools/pom.xml | 2 +- transport/coap/pom.xml | 2 +- transport/http/pom.xml | 2 +- transport/lwm2m/pom.xml | 2 +- transport/mqtt/pom.xml | 2 +- transport/pom.xml | 2 +- transport/snmp/pom.xml | 2 +- ui-ngx/pom.xml | 2 +- 60 files changed, 61 insertions(+), 61 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index ee66296dd0..c2400f50e5 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT thingsboard application diff --git a/common/actor/pom.xml b/common/actor/pom.xml index 63baab7044..5a7c3fd665 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/cache/pom.xml b/common/cache/pom.xml index 498782bc35..3094e1601f 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index 41dcd05d01..a8a419aba3 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index 7cb3adcab7..64867e9020 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index 5b63866b51..7f95023057 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/data/pom.xml b/common/data/pom.xml index d93d2ffc48..a27011f890 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/discovery-api/pom.xml b/common/discovery-api/pom.xml index d3a2012a79..e66d86ff41 100644 --- a/common/discovery-api/pom.xml +++ b/common/discovery-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index 7c5a3221b7..a153ee7a9f 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml index c3f6c45294..fa5012a180 100644 --- a/common/edqs/pom.xml +++ b/common/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/message/pom.xml b/common/message/pom.xml index 2f7247d6bb..e34250fc23 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/pom.xml b/common/pom.xml index c8e4e6e0ed..1bfb9904ed 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT thingsboard common diff --git a/common/proto/pom.xml b/common/proto/pom.xml index b5dc55d976..ad2243776b 100644 --- a/common/proto/pom.xml +++ b/common/proto/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index cb6de43d08..e934be308e 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/script/pom.xml b/common/script/pom.xml index e0473ca351..999d895513 100644 --- a/common/script/pom.xml +++ b/common/script/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml index 9a4d582418..d92e8e78cd 100644 --- a/common/script/remote-js-client/pom.xml +++ b/common/script/remote-js-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0 + 4.2.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index 3c287cc8a8..bda91efa78 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0 + 4.2.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/stats/pom.xml b/common/stats/pom.xml index 58872d35b8..f2d18688fa 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index e64e4e49c2..c4f6f85e64 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0 + 4.2.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index 73f27ff23d..6cdc499a41 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0 + 4.2.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 56f21148fe..72a5731f8f 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0 + 4.2.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 71694edbc8..71a1de3228 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0 + 4.2.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/pom.xml b/common/transport/pom.xml index 5fa05d254f..dcb627b1b0 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index 7976642d0e..ecf85b1dac 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 4.2.0 + 4.2.0-SNAPSHOT transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index e4f6208437..366d2b3d9b 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0 + 4.2.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/util/pom.xml b/common/util/pom.xml index 4da4ce36c3..7727b88694 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT common org.thingsboard.common diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml index 951f03b2b4..5d6079b313 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT common org.thingsboard.common diff --git a/dao/pom.xml b/dao/pom.xml index 473e81c447..187c7e6ea5 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT thingsboard dao diff --git a/edqs/pom.xml b/edqs/pom.xml index 5b1e6607d3..4cca1bd89e 100644 --- a/edqs/pom.xml +++ b/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT thingsboard edqs diff --git a/monitoring/pom.xml b/monitoring/pom.xml index 3a24077afa..9394c637f8 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -21,7 +21,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT thingsboard diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index e7e6a2d9e5..d3b47daa47 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/edqs/pom.xml b/msa/edqs/pom.xml index 81382199c6..882ba588e1 100644 --- a/msa/edqs/pom.xml +++ b/msa/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index bcd4f01d8a..95e4516044 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml index 7a1c99efc0..a7a8ff1b89 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT msa diff --git a/msa/pom.xml b/msa/pom.xml index 9961214a7b..b980bd703e 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 35b34617df..9f5c7cf358 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 0a6eb11e28..421cf1290f 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index 792d215793..f0a46069db 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0 + 4.2.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index 6f39a19944..45f9323ab7 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0 + 4.2.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index ef10b2da54..b95402ba14 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0 + 4.2.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 068ba3b193..2fbfc5b9f2 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0 + 4.2.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index 1e31c86f06..5b57136d88 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index ad27d9f3b6..ee79717ca4 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 4.2.0 + 4.2.0-SNAPSHOT org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index dfcbae8856..b81504aa06 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index 1bda1fc434..e15fc6699c 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index e5001c5c64..4aeb5227c8 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index 3cba6f359f..b30f1e2da3 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT thingsboard netty-mqtt - 4.2.0 + 4.2.0-SNAPSHOT jar Netty MQTT Client diff --git a/pom.xml b/pom.xml index 681a7fd69a..c4c71cfa63 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 4.2.0 + 4.2.0-SNAPSHOT pom Thingsboard diff --git a/rest-client/pom.xml b/rest-client/pom.xml index 3ed87b9fab..392c3b360f 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT thingsboard rest-client diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index eb9c35846d..aa561832b3 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index f5b60bc653..6ab5e66904 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 56fe5628ce..f2edca7b94 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/tools/pom.xml b/tools/pom.xml index 3e81be82a1..d748e649ab 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index 60b576aadf..1d829679ae 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 31bf0e6ebb..10ffdfc07e 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index 0beac3a450..1c83f8b443 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index 33a0bec8cc..ea8c750b6d 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/pom.xml b/transport/pom.xml index 6f93a832ac..eeb4873e48 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index 1af7fe6acf..071c4423b6 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT transport diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index beaa2314ba..772921ee7a 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0 + 4.2.0-SNAPSHOT thingsboard org.thingsboard From 00d32c23741b95608129638cabb6b5c73150d3ff Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 9 Jun 2025 17:11:52 +0300 Subject: [PATCH 040/235] 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 041/235] 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 042/235] 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 043/235] 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 044/235] 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 045/235] 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: Tue, 10 Jun 2025 13:03:39 +0300 Subject: [PATCH 046/235] added support for AlarmCountQuery entityFilter and keyFilters --- .../query/DefaultEntityQueryService.java | 17 +++ .../controller/EntityQueryControllerTest.java | 127 ++++++++++++++++++ .../common/data/query/AlarmCountQuery.java | 4 + 3 files changed, 148 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java index f90c90df54..f8220fa505 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -208,9 +208,26 @@ public class DefaultEntityQueryService implements EntityQueryService { @Override public long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) { + if (query.getEntityFilter() != null) { + EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query); + PageData entities = entityService.findEntityDataByQuery(securityUser.getTenantId(), + securityUser.getCustomerId(), entityDataQuery); + if (entities.getTotalElements() > 0) { + List entityIds = entities.getData().stream().map(EntityData::getEntityId).toList(); + return alarmService.countAlarmsByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query, entityIds); + } else { + return 0; + } + } return alarmService.countAlarmsByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query); } + private EntityDataQuery buildEntityDataQuery(AlarmCountQuery query) { + EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, + new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, ModelConstants.CREATED_TIME_PROPERTY))); + return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters()); + } + private EntityDataQuery buildEntityDataQuery(AlarmDataQuery query) { EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder(); EntityDataSortOrder entitiesSortOrder; diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 26c02e8704..035b5314e5 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.page.PageData; @@ -230,6 +231,60 @@ public class EntityQueryControllerTest extends AbstractControllerTest { testCountAlarmsByQuery(alarms); } + @Test + public void testTenantCountAlarmsWithEntityFilter() throws Exception { + loginTenantAdmin(); + List devices = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Device device = new Device(); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + Device savedDevice = doPost("/api/device", device, Device.class); + devices.add(savedDevice); + Thread.sleep(1); + + Alarm alarm = new Alarm(); + alarm.setOriginator(savedDevice.getId()); + alarm.setType("alarm" + i); + alarm.setSeverity(AlarmSeverity.WARNING); + doPost("/api/alarm", alarm, Alarm.class); + Thread.sleep(1); + } + + List assets = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Asset asset = new Asset(); + asset.setName("Asset" + i); + asset.setType("default"); + asset.setLabel("testLabel" + (int) (Math.random() * 1000)); + Asset savedAsset = doPost("/api/asset", asset, Asset.class); + assets.add(savedAsset); + Thread.sleep(1); + + Alarm alarm = new Alarm(); + alarm.setOriginator(savedAsset.getId()); + alarm.setType("alarm" + i); + alarm.setSeverity(AlarmSeverity.WARNING); + doPost("/api/alarm", alarm, Alarm.class); + Thread.sleep(1); + } + + EntityTypeFilter assetTypeFilter = new EntityTypeFilter(); + assetTypeFilter.setEntityType(EntityType.ASSET); + AlarmCountQuery assetAlarmQuery = new AlarmCountQuery(assetTypeFilter); + + Long assetAlamCount = doPostWithResponse("/api/alarmsQuery/count", assetAlarmQuery, Long.class); + Assert.assertEquals(assets.size(), assetAlamCount.longValue()); + + KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1"); + List keyFilters = Collections.singletonList(nameFilter); + AlarmCountQuery filteredAssetAlarmQuery = new AlarmCountQuery(assetTypeFilter, keyFilters); + + Long filteredAssetAlamCount = doPostWithResponse("/api/alarmsQuery/count", filteredAssetAlarmQuery, Long.class); + Assert.assertEquals(1, filteredAssetAlamCount.longValue()); + } + @Test public void testCustomerCountAlarmsByQuery() throws Exception { loginTenantAdmin(); @@ -259,6 +314,68 @@ public class EntityQueryControllerTest extends AbstractControllerTest { testCountAlarmsByQuery(alarms); } + @Test + public void testCustomerCountAlarmsWithEntityFilter() throws Exception { + loginTenantAdmin(); + List devices = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Device device = new Device(); + device.setCustomerId(customerId); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(doPost("/api/device", device, Device.class)); + Thread.sleep(1); + } + + List assets = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Asset asset = new Asset(); + asset.setCustomerId(customerId); + asset.setName("Asset" + i); + asset.setType("default"); + asset.setLabel("testLabel" + (int) (Math.random() * 1000)); + assets.add(doPost("/api/asset", asset, Asset.class)); + Thread.sleep(1); + } + + loginCustomerUser(); + + for (int i = 0; i < devices.size(); i++) { + Alarm alarm = new Alarm(); + alarm.setCustomerId(customerId); + alarm.setOriginator(devices.get(i).getId()); + alarm.setType("alarm" + i); + alarm.setSeverity(AlarmSeverity.WARNING); + doPost("/api/alarm", alarm, Alarm.class); + Thread.sleep(1); + } + + for (int i = 0; i < assets.size(); i++) { + Alarm alarm = new Alarm(); + alarm.setCustomerId(customerId); + alarm.setOriginator(assets.get(i).getId()); + alarm.setType("alarm" + i); + alarm.setSeverity(AlarmSeverity.WARNING); + doPost("/api/alarm", alarm, Alarm.class); + Thread.sleep(1); + } + + EntityTypeFilter assetTypeFilter = new EntityTypeFilter(); + assetTypeFilter.setEntityType(EntityType.ASSET); + AlarmCountQuery assetAlarmQuery = new AlarmCountQuery(assetTypeFilter); + + Long assetAlamCount = doPostWithResponse("/api/alarmsQuery/count", assetAlarmQuery, Long.class); + Assert.assertEquals(10, assetAlamCount.longValue()); + + KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1"); + List keyFilters = Collections.singletonList(nameFilter); + AlarmCountQuery filteredAssetAlarmQuery = new AlarmCountQuery(assetTypeFilter, keyFilters); + + Long filteredAssetAlamCount = doPostWithResponse("/api/alarmsQuery/count", filteredAssetAlarmQuery, Long.class); + Assert.assertEquals(1, filteredAssetAlamCount.longValue()); + } + private void testCountAlarmsByQuery(List alarms) throws Exception { AlarmCountQuery countQuery = new AlarmCountQuery(); @@ -913,4 +1030,14 @@ public class EntityQueryControllerTest extends AbstractControllerTest { return numericFilter; } + private KeyFilter buildStringKeyFilter(EntityKeyType entityKeyType, String name, StringFilterPredicate.StringOperation operation, String value) { + KeyFilter nameFilter = new KeyFilter(); + nameFilter.setKey(new EntityKey(entityKeyType, name)); + StringFilterPredicate predicate = new StringFilterPredicate(); + predicate.setOperation(operation); + predicate.setValue(FilterPredicateValue.fromString(value)); + nameFilter.setPredicate(predicate); + return nameFilter; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java index 71a8e8ef33..1ab7ef5b34 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java @@ -45,4 +45,8 @@ public class AlarmCountQuery extends EntityCountQuery { super(entityFilter); } + public AlarmCountQuery(EntityFilter entityFilter, List keyFilters) { + super(entityFilter, keyFilters); + } + } From 27473619dbc11e9a9f5598a5cea61f7f34b59496 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 11 Jun 2025 09:54:33 +0300 Subject: [PATCH 047/235] 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 a39dc6e03a3f4f96b1996a6a4078e7f0d42364a1 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 11 Jun 2025 11:25:39 +0300 Subject: [PATCH 048/235] refactoring, added test for /api/alarmsQuery/find --- .../query/DefaultEntityQueryService.java | 2 +- .../controller/EntityQueryControllerTest.java | 148 ++++++++++++++++++ .../server/common/data/query/AlarmData.java | 8 + 3 files changed, 157 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java index f8220fa505..b3fe76f154 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -237,7 +237,7 @@ public class DefaultEntityQueryService implements EntityQueryService { entitiesSortOrder = sortOrder; } EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); - return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters()); + return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters()); } @Override diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 035b5314e5..bf1f9c3e82 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -41,6 +41,9 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmCountQuery; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.AlarmDataPageLink; +import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValueSourceType; @@ -376,6 +379,151 @@ public class EntityQueryControllerTest extends AbstractControllerTest { Assert.assertEquals(1, filteredAssetAlamCount.longValue()); } + @Test + public void testFindTenantAlarmsWithEntityFilter() throws Exception { + loginTenantAdmin(); + List devices = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Device device = new Device(); + device.setCustomerId(customerId); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(doPost("/api/device", device, Device.class)); + Thread.sleep(1); + } + + List assets = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Asset asset = new Asset(); + asset.setCustomerId(customerId); + asset.setName("Asset" + i); + asset.setType("default"); + asset.setLabel("testLabel" + (int) (Math.random() * 1000)); + assets.add(doPost("/api/asset", asset, Asset.class)); + Thread.sleep(1); + } + + for (int i = 0; i < devices.size(); i++) { + Alarm alarm = new Alarm(); + alarm.setOriginator(devices.get(i).getId()); + alarm.setType("alarm" + i); + alarm.setSeverity(AlarmSeverity.WARNING); + doPost("/api/alarm", alarm, Alarm.class); + Thread.sleep(1); + } + + List assetAlarmTypes = new ArrayList<>(); + for (int i = 0; i < assets.size(); i++) { + Alarm alarm = new Alarm(); + alarm.setOriginator(assets.get(i).getId()); + String type = "asset alarm" + i; + alarm.setType(type); + assetAlarmTypes.add(type); + alarm.setSeverity(AlarmSeverity.WARNING); + doPost("/api/alarm", alarm, Alarm.class); + Thread.sleep(1); + } + + AlarmDataPageLink pageLink = new AlarmDataPageLink(); + pageLink.setPage(0); + pageLink.setPageSize(100); + pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "assignee"))); + + List alarmFields = new ArrayList<>(); + alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type")); + + EntityTypeFilter assetTypeFilter = new EntityTypeFilter(); + assetTypeFilter.setEntityType(EntityType.ASSET); + AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields); + + PageData alarmPageData = doPostWithTypedResponse("/api/alarmsQuery/find", assetAlarmQuery, new TypeReference<>() { + }); + Assert.assertEquals(10, alarmPageData.getTotalElements()); + List retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList(); + assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes); + + KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1"); + List keyFilters = Collections.singletonList(nameFilter); + AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, alarmFields); + PageData filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { + }); + Assert.assertEquals(1, filteredAssetAlamData.getTotalElements()); + } + + @Test + public void testFindCustomerAlarmsWithEntityFilter() throws Exception { + loginTenantAdmin(); + List devices = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Device device = new Device(); + device.setCustomerId(customerId); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(doPost("/api/device", device, Device.class)); + Thread.sleep(1); + } + + List assets = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Asset asset = new Asset(); + asset.setCustomerId(customerId); + asset.setName("Asset" + i); + asset.setType("default"); + asset.setLabel("testLabel" + (int) (Math.random() * 1000)); + assets.add(doPost("/api/asset", asset, Asset.class)); + Thread.sleep(1); + } + + loginCustomerUser(); + + for (int i = 0; i < devices.size(); i++) { + Alarm alarm = new Alarm(); + alarm.setCustomerId(customerId); + alarm.setOriginator(devices.get(i).getId()); + alarm.setType("alarm" + i); + alarm.setSeverity(AlarmSeverity.WARNING); + doPost("/api/alarm", alarm, Alarm.class); + Thread.sleep(1); + } + + List assetAlarmTypes = new ArrayList<>(); + for (int i = 0; i < assets.size(); i++) { + Alarm alarm = new Alarm(); + alarm.setCustomerId(customerId); + alarm.setOriginator(assets.get(i).getId()); + String type = "asset alarm" + i; + alarm.setType(type); + assetAlarmTypes.add(type); + alarm.setSeverity(AlarmSeverity.WARNING); + doPost("/api/alarm", alarm, Alarm.class); + Thread.sleep(1); + } + + AlarmDataPageLink pageLink = new AlarmDataPageLink(); + pageLink.setPage(0); + pageLink.setPageSize(100); + pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "assignee"))); + + EntityTypeFilter assetTypeFilter = new EntityTypeFilter(); + assetTypeFilter.setEntityType(EntityType.ASSET); + AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, Collections.emptyList()); + + PageData alarmPageData = doPostWithTypedResponse("/api/alarmsQuery/find", assetAlarmQuery, new TypeReference<>() { + }); + Assert.assertEquals(10, alarmPageData.getTotalElements()); + List retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList(); + assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes); + + KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1"); + List keyFilters = Collections.singletonList(nameFilter); + AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, Collections.emptyList()); + PageData filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { + }); + Assert.assertEquals(1, filteredAssetAlamData.getTotalElements()); + } + private void testCountAlarmsByQuery(List alarms) throws Exception { AlarmCountQuery countQuery = new AlarmCountQuery(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmData.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmData.java index 31547246c6..c63682b899 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmData.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.query; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.EqualsAndHashCode; import lombok.Getter; import org.thingsboard.server.common.data.alarm.Alarm; @@ -34,6 +36,12 @@ public class AlarmData extends AlarmInfo { @Getter private final Map> latest; + @JsonCreator + public AlarmData(@JsonProperty("entityId") EntityId entityId, @JsonProperty("latest") Map> latest) { + this.entityId = entityId; + this.latest = latest; + } + public AlarmData(AlarmInfo main, AlarmData prototype) { super(main); this.entityId = prototype.entityId; From 7ffd468d0684b69a566acc1119053475e5466649 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 11 Jun 2025 15:13:59 +0300 Subject: [PATCH 049/235] 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 050/235] 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 051/235] 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 052/235] 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 053/235] 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 054/235] 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 055/235] 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 056/235] 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 057/235] 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 058/235] 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 059/235] 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 81464c86ca247079a3632eb8ae2af74d781c2ff2 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 17 Jun 2025 16:47:10 +0300 Subject: [PATCH 060/235] UI: Patch for autocoplete hasBackdrop --- .../patches/@angular+material+18.2.14.patch | 61 +++++++++++++++++++ ui-ngx/src/app/shared/shared.module.ts | 2 + 2 files changed, 63 insertions(+) create mode 100644 ui-ngx/patches/@angular+material+18.2.14.patch diff --git a/ui-ngx/patches/@angular+material+18.2.14.patch b/ui-ngx/patches/@angular+material+18.2.14.patch new file mode 100644 index 0000000000..db380983a2 --- /dev/null +++ b/ui-ngx/patches/@angular+material+18.2.14.patch @@ -0,0 +1,61 @@ +diff --git a/node_modules/@angular/material/autocomplete/index.d.ts b/node_modules/@angular/material/autocomplete/index.d.ts +index 1ebc198..f572ea2 100755 +--- a/node_modules/@angular/material/autocomplete/index.d.ts ++++ b/node_modules/@angular/material/autocomplete/index.d.ts +@@ -220,6 +220,10 @@ export declare interface MatAutocompleteDefaultOptions { + requireSelection?: boolean; + /** Class or list of classes to be applied to the autocomplete's overlay panel. */ + overlayPanelClass?: string | string[]; ++ ++ backdropClass?: string; ++ ++ hasBackdrop?: boolean; + /** Wheter icon indicators should be hidden for single-selection. */ + hideSingleSelectionIndicator?: boolean; + } +diff --git a/node_modules/@angular/material/esm2022/autocomplete/autocomplete-trigger.mjs b/node_modules/@angular/material/esm2022/autocomplete/autocomplete-trigger.mjs +index 193f5e4..833446a 100755 +--- a/node_modules/@angular/material/esm2022/autocomplete/autocomplete-trigger.mjs ++++ b/node_modules/@angular/material/esm2022/autocomplete/autocomplete-trigger.mjs +@@ -670,6 +670,8 @@ export class MatAutocompleteTrigger { + scrollStrategy: this._scrollStrategy(), + width: this._getPanelWidth(), + direction: this._dir ?? undefined, ++ hasBackdrop: this._defaults?.hasBackdrop, ++ backdropClass: this._defaults?.backdropClass, + panelClass: this._defaults?.overlayPanelClass, + }); + } +diff --git a/node_modules/@angular/material/esm2022/autocomplete/autocomplete.mjs b/node_modules/@angular/material/esm2022/autocomplete/autocomplete.mjs +index 3d919a4..5fd6b4d 100755 +--- a/node_modules/@angular/material/esm2022/autocomplete/autocomplete.mjs ++++ b/node_modules/@angular/material/esm2022/autocomplete/autocomplete.mjs +@@ -41,6 +41,7 @@ export function MAT_AUTOCOMPLETE_DEFAULT_OPTIONS_FACTORY() { + autoSelectActiveOption: false, + hideSingleSelectionIndicator: false, + requireSelection: false, ++ hasBackdrop: false, + }; + } + /** Autocomplete component. */ +diff --git a/node_modules/@angular/material/fesm2022/autocomplete.mjs b/node_modules/@angular/material/fesm2022/autocomplete.mjs +index e3d0253..36cd6d7 100755 +--- a/node_modules/@angular/material/fesm2022/autocomplete.mjs ++++ b/node_modules/@angular/material/fesm2022/autocomplete.mjs +@@ -65,6 +65,7 @@ function MAT_AUTOCOMPLETE_DEFAULT_OPTIONS_FACTORY() { + autoSelectActiveOption: false, + hideSingleSelectionIndicator: false, + requireSelection: false, ++ hasBackdrop: false, + }; + } + /** Autocomplete component. */ +@@ -926,6 +927,8 @@ class MatAutocompleteTrigger { + scrollStrategy: this._scrollStrategy(), + width: this._getPanelWidth(), + direction: this._dir ?? undefined, ++ hasBackdrop: this._defaults?.hasBackdrop, ++ backdropClass: this._defaults?.backdropClass, + panelClass: this._defaults?.overlayPanelClass, + }); + } diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index ec7219d78a..9fd6e4a71e 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -276,6 +276,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { provide: MAT_AUTOCOMPLETE_DEFAULT_OPTIONS, useValue: { + hasBackdrop: true, + backdropClass: 'cdk-overlay-transparent-backdrop', hideSingleSelectionIndicator: true } }, From d2d22a44c21b504cc681e6e88710c8664485e50f Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 17 Jun 2025 18:17:38 +0300 Subject: [PATCH 061/235] 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 062/235] 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 063/235] 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 064/235] 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 7dc8f107408aa50c17bac9a244fe5ac8369a6c1c Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 19 Jun 2025 17:09:08 +0300 Subject: [PATCH 065/235] 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 066/235] 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 067/235] 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 068/235] 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 069/235] 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: Mon, 23 Jun 2025 11:45:41 +0300 Subject: [PATCH 070/235] 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 071/235] 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 1343c4af3bc108e0a68b32cc0171ea0559d88202 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 23 Jun 2025 16:16:42 +0300 Subject: [PATCH 072/235] 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 073/235] 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 074/235] 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 075/235] 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 076/235] 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 077/235] 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 7f0cd6dccbae3ac095c3b485a1938551ed76de1e Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 24 Jun 2025 15:08:17 +0300 Subject: [PATCH 078/235] 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 079/235] 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 080/235] 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 081/235] 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 082/235] 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 71ec0a588a402faa0a644cd5a7e5c617b354c56f Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 25 Jun 2025 11:34:17 +0300 Subject: [PATCH 083/235] 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 d5c885dcde0749d7ed2a64ecf95aa6ad91efc9bf Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 25 Jun 2025 14:31:41 +0300 Subject: [PATCH 084/235] 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 bf46d974ab8b07f9a27e11277cd5758668d41e77 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 25 Jun 2025 16:11:29 +0300 Subject: [PATCH 085/235] 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 086/235] 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 087/235] 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 088/235] 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 089/235] 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 f2c6fd45d85e24a9246193972623cf49c827fd5a Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 26 Jun 2025 17:46:55 +0300 Subject: [PATCH 090/235] 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 091/235] 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 092/235] 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 093/235] 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 094/235] 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 095/235] 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 096/235] 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 097/235] 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 098/235] 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 099/235] 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 100/235] 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 68be1163b66a2d0d0d24447581f244afacbcf642 Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Tue, 1 Jul 2025 11:09:03 +0300 Subject: [PATCH 101/235] 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 102/235] 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 14:18:42 +0300 Subject: [PATCH 103/235] 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 104/235] 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 105/235] 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 106/235] 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 107/235] 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 108/235] 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 109/235] 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 110/235] 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 111/235] 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 112/235] 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 fb966bded0fbdadf5ebd43d7afddb0278cf36bfb Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Wed, 2 Jul 2025 12:46:50 +0300 Subject: [PATCH 113/235] 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 114/235] 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 ad6e29145a88434b8666406f18838d332a1d0011 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 4 Jul 2025 12:45:28 +0300 Subject: [PATCH 127/235] 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 128/235] 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 129/235] 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 130/235] 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 131/235] 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 d4ec3f8b396294b6495e8e68fafcd5ef401f0087 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 7 Jul 2025 20:11:52 +0300 Subject: [PATCH 132/235] 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 133/235] 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 134/235] 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 135/235] 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 136/235] 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 137/235] 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 349e7402a47c78ead512aa59f1c6fcd37f528460 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 8 Jul 2025 14:33:25 +0300 Subject: [PATCH 138/235] 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 7860f4575f4668cc81c072c3bfcfc4cacbcff289 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 8 Jul 2025 17:46:18 +0300 Subject: [PATCH 139/235] 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 140/235] 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 141/235] 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 142/235] 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 143/235] 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 144/235] 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 145/235] 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 146/235] 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 147/235] 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 148/235] 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 149/235] 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 150/235] 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 151/235] 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 152/235] 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 153/235] 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) { + + } }
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 index 4f89321c30..82fff06270 100644 --- 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 @@ -39,18 +39,11 @@ export const aiModelRoutes: Routes = [ } ]; -const routes: Routes = [ - { - path: 'ai-models', - redirectTo: '/settings/ai-models' - } -]; - @NgModule({ providers: [ AiModelsTableConfigResolver ], - imports: [RouterModule.forChild(routes)], + imports: [RouterModule.forChild(aiModelRoutes)], exports: [RouterModule], }) export class AiModelRoutingModule { } 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 608efd8ad4..b95f0eddad 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,13 +32,7 @@ mat-button *ngIf="!readonly && !disabled" class="tidy" (click)="minifyJSON()"> {{'js-func.mini' | translate }} - @if (iconHint) { - - } +