Merge pull request #13998 from jekka001/support-ai-model-sync
[Edge] Added AI model sync
This commit is contained in:
commit
a538d90e0f
@ -24,6 +24,7 @@ import org.thingsboard.server.cache.limits.RateLimitService;
|
||||
import org.thingsboard.server.cluster.TbClusterService;
|
||||
import org.thingsboard.server.common.data.edge.EdgeEventType;
|
||||
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
|
||||
import org.thingsboard.server.dao.ai.AiModelService;
|
||||
import org.thingsboard.server.dao.alarm.AlarmCommentService;
|
||||
import org.thingsboard.server.dao.alarm.AlarmService;
|
||||
import org.thingsboard.server.dao.asset.AssetProfileService;
|
||||
@ -59,6 +60,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
|
||||
import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings;
|
||||
import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
|
||||
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
|
||||
import org.thingsboard.server.service.edge.rpc.processor.ai.AiModelProcessor;
|
||||
import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmProcessor;
|
||||
import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor;
|
||||
import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor;
|
||||
@ -261,6 +263,11 @@ public class EdgeContextComponent {
|
||||
@Autowired
|
||||
private CalculatedFieldProcessor calculatedFieldProcessor;
|
||||
|
||||
@Autowired
|
||||
private AiModelService aiModelService;
|
||||
@Autowired
|
||||
private AiModelProcessor aiModelProcessor;
|
||||
|
||||
public EdgeProcessor getProcessor(EdgeEventType edgeEventType) {
|
||||
EdgeProcessor processor = processorMap.get(edgeEventType);
|
||||
if (processor == null) {
|
||||
|
||||
@ -113,7 +113,7 @@ public class EdgeEventSourcingListener {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL == entityType) {
|
||||
if (EntityType.TENANT == entityType || EntityType.EDGE == entityType) {
|
||||
return;
|
||||
}
|
||||
log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event);
|
||||
@ -227,7 +227,7 @@ public class EdgeEventSourcingListener {
|
||||
break;
|
||||
case TENANT:
|
||||
return !event.getCreated();
|
||||
case API_USAGE_STATE, EDGE, AI_MODEL:
|
||||
case API_USAGE_STATE, EDGE:
|
||||
return false;
|
||||
case DOMAIN:
|
||||
if (entity instanceof Domain domain) {
|
||||
|
||||
@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.TbResource;
|
||||
import org.thingsboard.server.common.data.Tenant;
|
||||
import org.thingsboard.server.common.data.TenantProfile;
|
||||
import org.thingsboard.server.common.data.User;
|
||||
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.asset.Asset;
|
||||
@ -52,6 +53,7 @@ import org.thingsboard.server.common.data.cf.CalculatedField;
|
||||
import org.thingsboard.server.common.data.domain.DomainInfo;
|
||||
import org.thingsboard.server.common.data.edge.Edge;
|
||||
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
|
||||
import org.thingsboard.server.common.data.id.AiModelId;
|
||||
import org.thingsboard.server.common.data.id.AssetId;
|
||||
import org.thingsboard.server.common.data.id.AssetProfileId;
|
||||
import org.thingsboard.server.common.data.id.CalculatedFieldId;
|
||||
@ -86,6 +88,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
|
||||
import org.thingsboard.server.common.data.security.UserCredentials;
|
||||
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
|
||||
import org.thingsboard.server.common.data.widget.WidgetsBundle;
|
||||
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
|
||||
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
|
||||
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
|
||||
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
|
||||
@ -654,4 +657,17 @@ public class EdgeMsgConstructorUtils {
|
||||
.setIdLSB(calculatedFieldId.getId().getLeastSignificantBits()).build();
|
||||
}
|
||||
|
||||
public static AiModelUpdateMsg constructAiModelUpdatedMsg(UpdateMsgType msgType, AiModel aiModel) {
|
||||
return AiModelUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(aiModel))
|
||||
.setIdMSB(aiModel.getId().getId().getMostSignificantBits())
|
||||
.setIdLSB(aiModel.getId().getId().getLeastSignificantBits()).build();
|
||||
}
|
||||
|
||||
public static AiModelUpdateMsg constructAiModelDeleteMsg(AiModelId aiModelId) {
|
||||
return AiModelUpdateMsg.newBuilder()
|
||||
.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE)
|
||||
.setIdMSB(aiModelId.getId().getMostSignificantBits())
|
||||
.setIdLSB(aiModelId.getId().getLeastSignificantBits()).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.page.PageLink;
|
||||
import org.thingsboard.server.common.data.page.TimePageLink;
|
||||
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
|
||||
import org.thingsboard.server.dao.edge.stats.EdgeStatsKey;
|
||||
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
|
||||
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
|
||||
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
|
||||
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
|
||||
@ -934,6 +935,11 @@ public abstract class EdgeGrpcSession implements Closeable {
|
||||
result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg));
|
||||
}
|
||||
}
|
||||
if (uplinkMsg.getAiModelUpdateMsgCount() > 0) {
|
||||
for (AiModelUpdateMsg aiModelUpdateMsg : uplinkMsg.getAiModelUpdateMsgList()) {
|
||||
result.add(ctx.getAiModelProcessor().processAiModelMsgFromEdge(edge.getTenantId(), edge, aiModelUpdateMsg));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
|
||||
log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e);
|
||||
|
||||
@ -139,7 +139,7 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
|
||||
UPDATED_COMMENT, DELETED -> true;
|
||||
default -> switch (type) {
|
||||
case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE,
|
||||
WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, NOTIFICATION_TEMPLATE,
|
||||
WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, AI_MODEL, NOTIFICATION_TEMPLATE,
|
||||
NOTIFICATION_TARGET, NOTIFICATION_RULE -> true;
|
||||
default -> false;
|
||||
};
|
||||
|
||||
@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Copyright © 2016-2025 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.service.edge.rpc.processor.ai;
|
||||
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.util.Pair;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.server.common.data.EdgeUtils;
|
||||
import org.thingsboard.server.common.data.ai.AiModel;
|
||||
import org.thingsboard.server.common.data.edge.Edge;
|
||||
import org.thingsboard.server.common.data.edge.EdgeEvent;
|
||||
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
|
||||
import org.thingsboard.server.common.data.edge.EdgeEventType;
|
||||
import org.thingsboard.server.common.data.id.AiModelId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.msg.TbMsgType;
|
||||
import org.thingsboard.server.common.msg.TbMsgMetaData;
|
||||
import org.thingsboard.server.dao.exception.DataValidationException;
|
||||
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
|
||||
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
|
||||
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
|
||||
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
|
||||
import org.thingsboard.server.queue.util.TbCoreComponent;
|
||||
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@TbCoreComponent
|
||||
public class AiModelEdgeProcessor extends BaseAiModelProcessor implements AiModelProcessor {
|
||||
|
||||
@Override
|
||||
public ListenableFuture<Void> processAiModelMsgFromEdge(TenantId tenantId, Edge edge, AiModelUpdateMsg aiModelUpdateMsg) {
|
||||
AiModelId aiModelId = new AiModelId(new UUID(aiModelUpdateMsg.getIdMSB(), aiModelUpdateMsg.getIdLSB()));
|
||||
try {
|
||||
edgeSynchronizationManager.getEdgeId().set(edge.getId());
|
||||
|
||||
switch (aiModelUpdateMsg.getMsgType()) {
|
||||
case ENTITY_CREATED_RPC_MESSAGE:
|
||||
case ENTITY_UPDATED_RPC_MESSAGE:
|
||||
processAiModel(tenantId, aiModelId, aiModelUpdateMsg, edge);
|
||||
return Futures.immediateFuture(null);
|
||||
case UNRECOGNIZED:
|
||||
default:
|
||||
return handleUnsupportedMsgType(aiModelUpdateMsg.getMsgType());
|
||||
}
|
||||
} catch (DataValidationException e) {
|
||||
return Futures.immediateFailedFuture(e);
|
||||
} finally {
|
||||
edgeSynchronizationManager.getEdgeId().remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
|
||||
AiModelId aiModelId = new AiModelId(edgeEvent.getEntityId());
|
||||
switch (edgeEvent.getAction()) {
|
||||
case ADDED, UPDATED -> {
|
||||
Optional<AiModel> aiModel = edgeCtx.getAiModelService().findAiModelById(edgeEvent.getTenantId(), aiModelId);
|
||||
if (aiModel.isPresent()) {
|
||||
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
|
||||
AiModelUpdateMsg aiModelUpdateMsg = EdgeMsgConstructorUtils.constructAiModelUpdatedMsg(msgType, aiModel.get());
|
||||
return DownlinkMsg.newBuilder()
|
||||
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
|
||||
.addAiModelUpdateMsg(aiModelUpdateMsg)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
case DELETED -> {
|
||||
AiModelUpdateMsg aiModelUpdateMsg = EdgeMsgConstructorUtils.constructAiModelDeleteMsg(aiModelId);
|
||||
return DownlinkMsg.newBuilder()
|
||||
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
|
||||
.addAiModelUpdateMsg(aiModelUpdateMsg)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EdgeEventType getEdgeEventType() {
|
||||
return EdgeEventType.AI_MODEL;
|
||||
}
|
||||
|
||||
private void processAiModel(TenantId tenantId, AiModelId aiModelId, AiModelUpdateMsg aiModelUpdateMsg, Edge edge) {
|
||||
Pair<Boolean, Boolean> resultPair = super.saveOrUpdateAiModel(tenantId, aiModelId, aiModelUpdateMsg);
|
||||
Boolean wasCreated = resultPair.getFirst();
|
||||
if (wasCreated) {
|
||||
pushAiModelCreatedEventToRuleEngine(tenantId, edge, aiModelId);
|
||||
}
|
||||
Boolean nameWasUpdated = resultPair.getSecond();
|
||||
if (nameWasUpdated) {
|
||||
saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.AI_MODEL, EdgeEventActionType.UPDATED, aiModelId, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void pushAiModelCreatedEventToRuleEngine(TenantId tenantId, Edge edge, AiModelId aiModelId) {
|
||||
try {
|
||||
Optional<AiModel> aiModel = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId);
|
||||
if (aiModel.isPresent()) {
|
||||
String aiModelAsString = JacksonUtil.toString(aiModel.get());
|
||||
TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, edge.getCustomerId());
|
||||
pushEntityEventToRuleEngine(tenantId, aiModelId, edge.getCustomerId(), TbMsgType.ENTITY_CREATED, aiModelAsString, msgMetaData);
|
||||
} else {
|
||||
log.warn("[{}][{}] Failed to find aiModel", tenantId, aiModelId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[{}][{}] Failed to push aiModel action to rule engine: {}", tenantId, aiModelId, TbMsgType.ENTITY_CREATED.name(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright © 2016-2025 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.service.edge.rpc.processor.ai;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import org.thingsboard.server.common.data.edge.Edge;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
|
||||
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
|
||||
|
||||
public interface AiModelProcessor extends EdgeProcessor {
|
||||
|
||||
ListenableFuture<Void> processAiModelMsgFromEdge(TenantId tenantId, Edge edge, AiModelUpdateMsg aiModelUpdateMsg);
|
||||
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright © 2016-2025 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.service.edge.rpc.processor.ai;
|
||||
|
||||
import com.datastax.oss.driver.api.core.uuid.Uuids;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.util.Pair;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.server.common.data.StringUtils;
|
||||
import org.thingsboard.server.common.data.ai.AiModel;
|
||||
import org.thingsboard.server.common.data.id.AiModelId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.dao.service.DataValidator;
|
||||
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
|
||||
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
public abstract class BaseAiModelProcessor extends BaseEdgeProcessor {
|
||||
|
||||
@Autowired
|
||||
private DataValidator<AiModel> aiModelValidator;
|
||||
|
||||
protected Pair<Boolean, Boolean> saveOrUpdateAiModel(TenantId tenantId, AiModelId aiModelId, AiModelUpdateMsg aiModelUpdateMsg) {
|
||||
boolean isCreated = false;
|
||||
boolean isNameUpdated = false;
|
||||
try {
|
||||
AiModel aiModel = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true);
|
||||
if (aiModel == null) {
|
||||
throw new RuntimeException("[{" + tenantId + "}] aiModelUpdateMsg {" + aiModelUpdateMsg + " } cannot be converted to aiModel");
|
||||
}
|
||||
|
||||
Optional<AiModel> aiModelById = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId);
|
||||
if (aiModelById.isEmpty()) {
|
||||
aiModel.setCreatedTime(Uuids.unixTimestamp(aiModelId.getId()));
|
||||
isCreated = true;
|
||||
aiModel.setId(null);
|
||||
} else {
|
||||
aiModel.setId(aiModelId);
|
||||
}
|
||||
|
||||
String aiModelName = aiModel.getName();
|
||||
Optional<AiModel> aiModelByName = edgeCtx.getAiModelService().findAiModelByTenantIdAndName(aiModel.getTenantId(), aiModelName);
|
||||
if (aiModelByName.isPresent() && !aiModelByName.get().getId().equals(aiModelId)) {
|
||||
aiModelName = aiModelName + "_" + StringUtils.randomAlphabetic(15);
|
||||
log.warn("[{}] aiModel with name {} already exists. Renaming aiModel name to {}",
|
||||
tenantId, aiModel.getName(), aiModelByName.get().getName());
|
||||
isNameUpdated = true;
|
||||
}
|
||||
aiModel.setName(aiModelName);
|
||||
|
||||
aiModelValidator.validate(aiModel, AiModel::getTenantId);
|
||||
|
||||
if (isCreated) {
|
||||
aiModel.setId(aiModelId);
|
||||
}
|
||||
|
||||
edgeCtx.getAiModelService().save(aiModel, false);
|
||||
} catch (Exception e) {
|
||||
log.error("[{}] Failed to process aiModel update msg [{}]", tenantId, aiModelUpdateMsg, e);
|
||||
throw e;
|
||||
}
|
||||
return Pair.of(isCreated, isNameUpdated);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Copyright © 2016-2025 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.edge;
|
||||
|
||||
import com.datastax.oss.driver.api.core.uuid.Uuids;
|
||||
import com.google.protobuf.AbstractMessage;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.server.common.data.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.id.TenantId;
|
||||
import org.thingsboard.server.dao.service.DaoSqlTest;
|
||||
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
|
||||
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
|
||||
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
|
||||
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@DaoSqlTest
|
||||
public class AiModelEdgeTest extends AbstractEdgeTest {
|
||||
|
||||
private static final String DEFAULT_AI_MODEL_NAME = "Edge Test AiModel";
|
||||
private static final String UPDATED_AI_MODEL_NAME = "Updated Edge Test AiModel";
|
||||
|
||||
@Test
|
||||
public void testAiModel_create_update_delete() throws Exception {
|
||||
// create AiModel
|
||||
AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
|
||||
|
||||
edgeImitator.expectMessageAmount(1);
|
||||
AiModel savedAiModel = doPost("/api/ai/model", aiModel, AiModel.class);
|
||||
Assert.assertTrue(edgeImitator.waitForMessages());
|
||||
|
||||
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
|
||||
Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg);
|
||||
AiModelUpdateMsg aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage;
|
||||
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType());
|
||||
Assert.assertEquals(savedAiModel.getUuidId().getMostSignificantBits(), aiModelUpdateMsg.getIdMSB());
|
||||
Assert.assertEquals(savedAiModel.getUuidId().getLeastSignificantBits(), aiModelUpdateMsg.getIdLSB());
|
||||
AiModel aiModelFromMsg = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true);
|
||||
Assert.assertNotNull(aiModelFromMsg);
|
||||
|
||||
Assert.assertEquals(DEFAULT_AI_MODEL_NAME, aiModelFromMsg.getName());
|
||||
Assert.assertEquals(savedAiModel.getTenantId(), aiModelFromMsg.getTenantId());
|
||||
|
||||
// update AiModel
|
||||
edgeImitator.expectMessageAmount(1);
|
||||
savedAiModel.setName(UPDATED_AI_MODEL_NAME);
|
||||
savedAiModel = doPost("/api/ai/model", savedAiModel, AiModel.class);
|
||||
Assert.assertTrue(edgeImitator.waitForMessages());
|
||||
|
||||
latestMessage = edgeImitator.getLatestMessage();
|
||||
Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg);
|
||||
aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage;
|
||||
aiModelFromMsg = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true);
|
||||
Assert.assertNotNull(aiModelFromMsg);
|
||||
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType());
|
||||
Assert.assertEquals(UPDATED_AI_MODEL_NAME, aiModelFromMsg.getName());
|
||||
|
||||
// delete AiModel
|
||||
edgeImitator.expectMessageAmount(1);
|
||||
doDelete("/api/ai/model/" + savedAiModel.getUuidId())
|
||||
.andExpect(status().isOk());
|
||||
Assert.assertTrue(edgeImitator.waitForMessages());
|
||||
|
||||
latestMessage = edgeImitator.getLatestMessage();
|
||||
Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg);
|
||||
aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage;
|
||||
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType());
|
||||
Assert.assertEquals(savedAiModel.getUuidId().getMostSignificantBits(), aiModelUpdateMsg.getIdMSB());
|
||||
Assert.assertEquals(savedAiModel.getUuidId().getLeastSignificantBits(), aiModelUpdateMsg.getIdLSB());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendAiModelToCloud() throws Exception {
|
||||
AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
|
||||
UUID uuid = Uuids.timeBased();
|
||||
UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
|
||||
|
||||
checkAiModelOnCloud(uplinkMsg, uuid, aiModel.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateAiModelNameOnCloud() throws Exception {
|
||||
AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
|
||||
UUID uuid = Uuids.timeBased();
|
||||
UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
|
||||
|
||||
checkAiModelOnCloud(uplinkMsg, uuid, aiModel.getName());
|
||||
|
||||
aiModel.setName(UPDATED_AI_MODEL_NAME);
|
||||
UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
|
||||
|
||||
checkAiModelOnCloud(updatedUplinkMsg, uuid, aiModel.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAiModelToCloudWithNameThatAlreadyExistsOnCloud() throws Exception {
|
||||
AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
|
||||
|
||||
edgeImitator.expectMessageAmount(1);
|
||||
AiModel savedAiModel = doPost("/api/ai/model", aiModel, AiModel.class);
|
||||
Assert.assertTrue(edgeImitator.waitForMessages());
|
||||
|
||||
UUID uuid = Uuids.timeBased();
|
||||
|
||||
UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
|
||||
|
||||
edgeImitator.expectResponsesAmount(1);
|
||||
edgeImitator.expectMessageAmount(1);
|
||||
edgeImitator.sendUplinkMsg(uplinkMsg);
|
||||
|
||||
Assert.assertTrue(edgeImitator.waitForResponses());
|
||||
Assert.assertTrue(edgeImitator.waitForMessages());
|
||||
|
||||
Optional<AiModelUpdateMsg> aiModelUpdateMsgOpt = edgeImitator.findMessageByType(AiModelUpdateMsg.class);
|
||||
Assert.assertTrue(aiModelUpdateMsgOpt.isPresent());
|
||||
AiModelUpdateMsg latestAiModelUpdateMsg = aiModelUpdateMsgOpt.get();
|
||||
AiModel aiModelFromMsg = JacksonUtil.fromString(latestAiModelUpdateMsg.getEntity(), AiModel.class, true);
|
||||
Assert.assertNotNull(aiModelFromMsg);
|
||||
Assert.assertNotEquals(DEFAULT_AI_MODEL_NAME, aiModelFromMsg.getName());
|
||||
|
||||
Assert.assertNotEquals(savedAiModel.getUuidId(), uuid);
|
||||
|
||||
AiModel aiModelFromCloud = doGet("/api/ai/model/" + uuid, AiModel.class);
|
||||
Assert.assertNotNull(aiModelFromCloud);
|
||||
Assert.assertNotEquals(DEFAULT_AI_MODEL_NAME, aiModelFromCloud.getName());
|
||||
}
|
||||
|
||||
private AiModel createSimpleAiModel(String name) {
|
||||
AiModel aiModel = new AiModel();
|
||||
aiModel.setTenantId(tenantId);
|
||||
aiModel.setName(name);
|
||||
aiModel.setConfiguration(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;
|
||||
}
|
||||
|
||||
private UplinkMsg getUplinkMsg(UUID uuid, AiModel aiModel, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException {
|
||||
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
|
||||
AiModelUpdateMsg.Builder aiModelUpdateMsgBuilder = AiModelUpdateMsg.newBuilder();
|
||||
aiModelUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
|
||||
aiModelUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
|
||||
aiModelUpdateMsgBuilder.setEntity(JacksonUtil.toString(aiModel));
|
||||
aiModelUpdateMsgBuilder.setMsgType(updateMsgType);
|
||||
testAutoGeneratedCodeByProtobuf(aiModelUpdateMsgBuilder);
|
||||
uplinkMsgBuilder.addAiModelUpdateMsg(aiModelUpdateMsgBuilder.build());
|
||||
|
||||
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
|
||||
|
||||
return uplinkMsgBuilder.build();
|
||||
}
|
||||
|
||||
private void checkAiModelOnCloud(UplinkMsg uplinkMsg, UUID uuid, String resourceTitle) throws Exception {
|
||||
edgeImitator.expectResponsesAmount(1);
|
||||
edgeImitator.sendUplinkMsg(uplinkMsg);
|
||||
|
||||
Assert.assertTrue(edgeImitator.waitForResponses());
|
||||
|
||||
UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
|
||||
Assert.assertTrue(latestResponseMsg.getSuccess());
|
||||
|
||||
AiModel aiModel = doGet("/api/ai/model/" + uuid, AiModel.class);
|
||||
Assert.assertNotNull(aiModel);
|
||||
Assert.assertEquals(resourceTitle, aiModel.getName());
|
||||
}
|
||||
|
||||
}
|
||||
@ -29,6 +29,7 @@ import org.thingsboard.edge.rpc.EdgeGrpcClient;
|
||||
import org.thingsboard.edge.rpc.EdgeRpcClient;
|
||||
import org.thingsboard.server.controller.AbstractWebTest;
|
||||
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
|
||||
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
|
||||
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
|
||||
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
|
||||
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
|
||||
@ -358,6 +359,11 @@ public class EdgeImitator {
|
||||
result.add(saveDownlinkMsg(calculatedFieldUpdateMsg));
|
||||
}
|
||||
}
|
||||
if (downlinkMsg.getAiModelUpdateMsgCount() > 0) {
|
||||
for (AiModelUpdateMsg aiModelUpdateMsg : downlinkMsg.getAiModelUpdateMsgList()) {
|
||||
result.add(saveDownlinkMsg(aiModelUpdateMsg));
|
||||
}
|
||||
}
|
||||
if (downlinkMsg.hasEdgeConfiguration()) {
|
||||
result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration()));
|
||||
}
|
||||
|
||||
@ -29,6 +29,8 @@ public interface AiModelService extends EntityDaoService {
|
||||
|
||||
AiModel save(AiModel model);
|
||||
|
||||
AiModel save(AiModel model, boolean doValidate);
|
||||
|
||||
Optional<AiModel> findAiModelById(TenantId tenantId, AiModelId modelId);
|
||||
|
||||
PageData<AiModel> findAiModelsByTenantId(TenantId tenantId, PageLink pageLink);
|
||||
@ -37,6 +39,8 @@ public interface AiModelService extends EntityDaoService {
|
||||
|
||||
FluentFuture<Optional<AiModel>> findAiModelByTenantIdAndIdAsync(TenantId tenantId, AiModelId modelId);
|
||||
|
||||
Optional<AiModel> findAiModelByTenantIdAndName(TenantId tenantId, String name);
|
||||
|
||||
boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId);
|
||||
|
||||
}
|
||||
|
||||
@ -47,7 +47,8 @@ public enum EdgeEventType {
|
||||
TB_RESOURCE(true, EntityType.TB_RESOURCE),
|
||||
OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT),
|
||||
DOMAIN(true, EntityType.DOMAIN),
|
||||
CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD);
|
||||
CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD),
|
||||
AI_MODEL(true, EntityType.AI_MODEL);
|
||||
|
||||
private final boolean allEdgesRelated;
|
||||
|
||||
|
||||
@ -113,6 +113,7 @@ public class EntityIdFactory {
|
||||
case OAUTH2_CLIENT -> new OAuth2ClientId(uuid);
|
||||
case DOMAIN -> new DomainId(uuid);
|
||||
case CALCULATED_FIELD -> new CalculatedFieldId(uuid);
|
||||
case AI_MODEL -> new AiModelId(uuid);
|
||||
default -> throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!");
|
||||
};
|
||||
}
|
||||
|
||||
@ -136,7 +136,7 @@ public class EdgeGrpcClient implements EdgeRpcClient {
|
||||
.setConnectRequestMsg(ConnectRequestMsg.newBuilder()
|
||||
.setEdgeRoutingKey(edgeKey)
|
||||
.setEdgeSecret(edgeSecret)
|
||||
.setEdgeVersion(EdgeVersion.V_4_2_0)
|
||||
.setEdgeVersion(EdgeVersion.V_4_3_0)
|
||||
.setMaxInboundMessageSize(maxInboundMessageSize)
|
||||
.build())
|
||||
.build());
|
||||
|
||||
@ -44,6 +44,7 @@ enum EdgeVersion {
|
||||
V_4_0_0 = 10;
|
||||
V_4_1_0 = 11;
|
||||
V_4_2_0 = 12;
|
||||
V_4_3_0 = 13;
|
||||
|
||||
V_LATEST = 999;
|
||||
}
|
||||
@ -133,6 +134,12 @@ message CalculatedFieldUpdateMsg{
|
||||
string entity = 4;
|
||||
}
|
||||
|
||||
message AiModelUpdateMsg{
|
||||
UpdateMsgType msgType = 1;
|
||||
int64 idMSB = 2;
|
||||
int64 idLSB = 3;
|
||||
string entity = 4;
|
||||
}
|
||||
|
||||
message EntityDataProto {
|
||||
int64 entityIdMSB = 1;
|
||||
@ -441,6 +448,7 @@ message UplinkMsg {
|
||||
repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24;
|
||||
repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25;
|
||||
repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26;
|
||||
repeated AiModelUpdateMsg aiModelUpdateMsg = 27;
|
||||
}
|
||||
|
||||
message UplinkResponseMsg {
|
||||
@ -491,4 +499,5 @@ message DownlinkMsg {
|
||||
repeated NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = 33;
|
||||
repeated OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = 34;
|
||||
repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 35;
|
||||
repeated AiModelUpdateMsg aiModelUpdateMsg = 36;
|
||||
}
|
||||
|
||||
@ -29,12 +29,15 @@ 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.AiModelEntity;
|
||||
import org.thingsboard.server.dao.service.DataValidator;
|
||||
import org.thingsboard.server.dao.sql.JpaExecutorService;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
|
||||
|
||||
@ -63,11 +66,23 @@ class AiModelServiceImpl extends CachedVersionedEntityService<AiModelCacheKey, A
|
||||
@Override
|
||||
@Transactional
|
||||
public AiModel save(AiModel model) {
|
||||
aiModelValidator.validate(model, AiModel::getTenantId);
|
||||
return save(model, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiModel save(AiModel aiModel, boolean doValidate) {
|
||||
AiModel oldAiModel = null;
|
||||
if (doValidate) {
|
||||
oldAiModel = aiModelValidator.validate(aiModel, AiModel::getTenantId);
|
||||
} else if (aiModel.getId() != null) {
|
||||
oldAiModel = findAiModelById(aiModel.getTenantId(), aiModel.getId()).orElse(null);
|
||||
}
|
||||
|
||||
AiModel savedModel;
|
||||
try {
|
||||
savedModel = aiModelDao.saveAndFlush(model.getTenantId(), model);
|
||||
savedModel = aiModelDao.saveAndFlush(aiModel.getTenantId(), aiModel);
|
||||
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedModel.getTenantId()).entityId(savedModel.getId())
|
||||
.entity(savedModel).oldEntity(oldAiModel).created(oldAiModel == null).broadcastEvent(true).build());
|
||||
} catch (Exception e) {
|
||||
checkConstraintViolation(e,
|
||||
"ai_model_name_unq_key", "AI model with such name already exist!",
|
||||
@ -103,10 +118,15 @@ class AiModelServiceImpl extends CachedVersionedEntityService<AiModelCacheKey, A
|
||||
return FluentFuture.from(jpaExecutor.submit(() -> findAiModelByTenantIdAndId(tenantId, modelId)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AiModel> findAiModelByTenantIdAndName(TenantId tenantId, String name) {
|
||||
return Optional.ofNullable(aiModelDao.findByTenantIdAndName(tenantId.getId(), name));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId) {
|
||||
return deleteByTenantIdAndIdInternal(tenantId, modelId);
|
||||
return deleteByTenantIdAndIdInternal(tenantId, modelId.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -123,14 +143,21 @@ class AiModelServiceImpl extends CachedVersionedEntityService<AiModelCacheKey, A
|
||||
@Override
|
||||
@Transactional
|
||||
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
|
||||
deleteByTenantIdAndIdInternal(tenantId, new AiModelId(id.getId()));
|
||||
deleteByTenantIdAndIdInternal(tenantId, 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)));
|
||||
private boolean deleteByTenantIdAndIdInternal(TenantId tenantId, UUID modelId) {
|
||||
AiModel aiModel = findAiModelById(tenantId, new AiModelId(modelId)).orElse(null);
|
||||
if (aiModel == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean deleted = aiModelDao.deleteByTenantIdAndId(tenantId, aiModel.getId());
|
||||
if (deleted) {
|
||||
publishEvictEvent(new AiModelCacheEvictEvent.Deleted(AiModelCacheKey.of(tenantId, aiModel.getId())));
|
||||
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(aiModel.getId()).entity(aiModel).build());
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
|
||||
|
||||
@ -74,6 +74,7 @@ import org.thingsboard.server.common.data.UpdateMessage;
|
||||
import org.thingsboard.server.common.data.UsageInfo;
|
||||
import org.thingsboard.server.common.data.User;
|
||||
import org.thingsboard.server.common.data.UserEmailInfo;
|
||||
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.AlarmCommentInfo;
|
||||
@ -98,6 +99,7 @@ import org.thingsboard.server.common.data.edge.EdgeInfo;
|
||||
import org.thingsboard.server.common.data.edge.EdgeInstructions;
|
||||
import org.thingsboard.server.common.data.edge.EdgeSearchQuery;
|
||||
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
|
||||
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;
|
||||
@ -4144,6 +4146,29 @@ public class RestClient implements Closeable {
|
||||
}
|
||||
}
|
||||
|
||||
public AiModel saveAiModel(AiModel aiModel) {
|
||||
return restTemplate.postForEntity(baseURL + "/api/ai/model", aiModel, AiModel.class).getBody();
|
||||
}
|
||||
|
||||
public Optional<AiModel> getAiModel(AiModelId aiModelId) {
|
||||
try {
|
||||
ResponseEntity<AiModel> response = restTemplate.getForEntity(
|
||||
baseURL + "/api/aiModel/{aiModelId}", AiModel.class, aiModelId.getId());
|
||||
return Optional.ofNullable(response.getBody());
|
||||
} catch (HttpClientErrorException exception) {
|
||||
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteAiModel(AiModelId aiModelId) {
|
||||
restTemplate.delete(baseURL + "/api/aiModel/{aiModelId}", aiModelId.getId());
|
||||
}
|
||||
|
||||
|
||||
private String getTimeUrlParams(TimePageLink pageLink) {
|
||||
String urlParams = getUrlParams(pageLink);
|
||||
if (pageLink.getStartTime() != null) {
|
||||
|
||||
@ -92,7 +92,6 @@ import static org.thingsboard.server.dao.service.ConstraintValidator.validateFie
|
||||
configClazz = TbAiNodeConfiguration.class,
|
||||
configDirective = "tbExternalNodeAiConfig",
|
||||
iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDkiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OSA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0zOC42MzExIDE3LjA3OTVDNDAuMTcwNSAxNy4wNzk2IDQxLjY1MTggMTcuNjg3MiA0Mi43NDc4IDE4Ljc3NjNDNDMuODQ0OCAxOS44NjYzIDQ0LjQ2NTkgMjEuMzUwMSA0NC40NjU5IDIyLjkwMjlWMzUuNDY1MkM0NC40NjU5IDM2LjM1MDkgNDQuMzU2NyAzNy4wNzY5IDQ0LjA5NzMgMzcuNzUxN0M0My44NDE0IDM4LjQxNjcgNDMuNDY1MSAzOC45NjE0IDQzLjA0NDggMzkuNTAyOEM0Mi40NjY3IDQwLjI0NzIgNDEuNjU2MyA0MC42ODU5IDQwLjg5MTkgNDAuOTM4OEM0MC4xMjExIDQxLjE5MzcgMzkuMzE0MyA0MS4yODg1IDM4LjYzMTEgNDEuMjg4NUgzMS4wMjU5TDIzLjM4MTIgNDUuODQ2NEMyMy4wNDMxIDQ2LjA0NzggMjIuNjI0MSA0Ni4wNTA3IDIyLjI4MzkgNDUuODUyOUMyMS45NDM3IDQ1LjY1NDcgMjEuNzMzOCA0NS4yODU5IDIxLjczMzcgNDQuODg3MlY0MS4yODg1SDE5LjY2NjNDMTguMTI2OSA0MS4yODg0IDE2LjY0NTUgNDAuNjgwOSAxNS41NDk2IDM5LjU5MThDMTQuNDUyNyAzOC41MDE5IDEzLjgzMTUgMzcuMDE3OSAxMy44MzE1IDM1LjQ2NTJWMjIuOTAyOUMxMy44MzE1IDIyLjMyMDIgMTMuOTE4NSAyMS43NDY4IDE0LjA4NTggMjEuMjAwN0wxNi4yODg5IDIxLjgxMDFMMTcuMjA5OSAyNS4yNTAyQzE3Ljk0MTYgMjcuOTg0NSAyMS43NTYyIDI3Ljk4NDQgMjIuNDg4IDI1LjI1MDJMMjMuNDA3OSAyMS44MTAxTDI2Ljc5MTcgMjAuODc0OUMyOC41NzkxIDIwLjM4MDUgMjkuMTc3IDE4LjUwMjYgMjguNTg4OCAxNy4wNzk1SDM4LjYzMTFaTTIyLjU4NDIgMzEuNTM5NUMyMS45OCAzMS41Mzk3IDIxLjQ5MDEgMzIuMDM3NiAyMS40OTAxIDMyLjY1MTlDMjEuNDkwMiAzMy4yNjYgMjEuOTgwMSAzMy43NjQgMjIuNTg0MiAzMy43NjQySDM0LjYxOTFDMzUuMjIzMyAzMy43NjQyIDM1LjcxMzEgMzMuMjY2MSAzNS43MTMyIDMyLjY1MTlDMzUuNzEzMiAzMi4wMzc1IDM1LjIyMzQgMzEuNTM5NSAzNC42MTkxIDMxLjUzOTVIMjIuNTg0MlpNMjQuNzcyMyAyNC44NjU3QzI0LjE2ODIgMjQuODY1OCAyMy42NzgzIDI1LjM2MzggMjMuNjc4MyAyNS45NzhDMjMuNjc4NCAyNi41OTIyIDI0LjE2ODMgMjcuMDkwMiAyNC43NzIzIDI3LjA5MDNIMzcuOTAxNEMzOC41MDU1IDI3LjA5MDMgMzguOTk1MyAyNi41OTIyIDM4Ljk5NTQgMjUuOTc4QzM4Ljk5NTQgMjUuMzYzNyAzOC41MDU2IDI0Ljg2NTcgMzcuOTAxNCAyNC44NjU3SDI0Ljc3MjNaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjc2Ii8+CjxwYXRoIGQ9Ik0xOC43ODkxIDExLjI5NzVDMTkuMDY5MSAxMC4xODA4IDIwLjYyOTkgMTAuMTgwOCAyMC45MDk5IDExLjI5NzVMMjEuOTE0MyAxNS4zMDM2QzIyLjAxMTYgMTUuNjkxOCAyMi4zMDY1IDE1Ljk5NzggMjIuNjg2NyAxNi4xMDNMMjYuMzYxMSAxNy4xMTg3QzI3LjQzNyAxNy40MTYyIDI3LjQzNyAxOC45Njc2IDI2LjM2MTEgMTkuMjY1MUwyMi42NzYxIDIwLjI4NEMyMi4zMDE4IDIwLjM4NzQgMjIuMDA4NyAyMC42ODQ1IDIxLjkwNjggMjEuMDY1TDIwLjkwNDYgMjQuODEyNUMyMC42MTE3IDI1LjkwNTggMTkuMDg2MSAyNS45MDU5IDE4Ljc5MzMgMjQuODEyNUwxNy43OTExIDIxLjA2NUMxNy42ODkzIDIwLjY4NDcgMTcuMzk3IDIwLjM4NzUgMTcuMDIyOSAyMC4yODRMMTMuMzM2OCAxOS4yNjUxQzEyLjI2MTQgMTguOTY3MyAxMi4yNjE1IDE3LjQxNjUgMTMuMzM2OCAxNy4xMTg3TDE3LjAxMTIgMTYuMTAzQzE3LjM5MTYgMTUuOTk3OCAxNy42ODc0IDE1LjY5MTkgMTcuNzg0NyAxNS4zMDM2TDE4Ljc4OTEgMTEuMjk3NVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNzYiLz4KPHBhdGggZD0iTTEwLjAzNDMgNy4wMjQyNUMxMC4zMDY4IDUuODk0NDQgMTEuODg2OCA1Ljg5NDQ0IDEyLjE1OTQgNy4wMjQyNUwxMi42OTg5IDkuMjYyOThDMTIuNzkyNyA5LjY1MTc0IDEzLjA4NTEgOS45NTg4NyAxMy40NjQgMTAuMDY3OUwxNS41NzczIDEwLjY3NTFDMTYuNjM5MyAxMC45ODAzIDE2LjYzOTMgMTIuNTEwOSAxNS41NzczIDEyLjgxNjFMMTMuNDUzMyAxMy40MjY1QzEzLjA4MDIgMTMuNTMzOCAxMi43OTA4IDEzLjgzMzkgMTIuNjkyNSAxNC4yMTUxTDEyLjE1NTEgMTYuMzA0QzExLjg3IDE3LjQxMTYgMTAuMzIzNiAxNy40MTE2IDEwLjAzODUgMTYuMzA0TDkuNTAwMDMgMTQuMjE1MUM5LjQwMTczIDEzLjgzMzkgOS4xMTIzNSAxMy41MzM3IDguNzM5MyAxMy40MjY1TDYuNjE1MjQgMTIuODE2MUM1LjU1Mzc4IDEyLjUxMDYgNS41NTM2NCAxMC45ODA0IDYuNjE1MjQgMTAuNjc1MUw4LjcyODYyIDEwLjA2NzlDOS4xMDc2IDkuOTU4OTggOS4zOTk3OCA5LjY1MTg0IDkuNDkzNjIgOS4yNjI5OEwxMC4wMzQzIDcuMDI0MjVaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjc2Ii8+CjxwYXRoIGQ9Ik0yNS45MDI4IDYuNzMzMTNDMjYuMTg3OCA1LjYyNTQxIDI3LjczNDMgNS42MjU0MSAyOC4wMTkzIDYuNzMzMTNMMjguMjAzMSA3LjQ0Njc5QzI4LjMwMyA3LjgzNDMxIDI4LjYwMDEgOC4xMzcwNSAyOC45ODA5IDguMjM5NzVMMjkuNTM0NCA4LjM4OTY1QzMwLjYxOTIgOC42ODIxMiAzMC42MTkzIDEwLjI0NjkgMjkuNTM0NCAxMC41MzkzTDI4Ljk2OTIgMTAuNjkxNEMyOC41OTQ0IDEwLjc5MjUgMjguMjk5OSAxMS4wODgzIDI4LjE5NTYgMTEuNDY4TDI4LjAxNTEgMTIuMTI4NUMyNy43MTc0IDEzLjIxMjggMjYuMjA0NyAxMy4yMTI4IDI1LjkwNyAxMi4xMjg1TDI1LjcyNTQgMTEuNDY4QzI1LjYyMTEgMTEuMDg4MiAyNS4zMjY4IDEwLjc5MjQgMjQuOTUxOCAxMC42OTE0TDI0LjM4NzcgMTAuNTM5M0MyMy4zMDI2IDEwLjI0NyAyMy4zMDI2IDguNjgxOTggMjQuMzg3NyA4LjM4OTY1TDI0Ljk0MDEgOC4yMzk3NUMyNS4zMjExIDguMTM3MDkgMjUuNjE5MSA3LjgzNDQ2IDI1LjcxOSA3LjQ0Njc5TDI1LjkwMjggNi43MzMxM1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNzYiLz4KPC9zdmc+Cg==",
|
||||
ruleChainTypes = RuleChainType.CORE,
|
||||
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/ai-request/"
|
||||
)
|
||||
public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user