AI Request Node: added ability to attach files (#13910)
This commit is contained in:
parent
07280bcc36
commit
ec30bb0578
@ -97,6 +97,7 @@ import org.thingsboard.server.dao.ota.OtaPackageService;
|
||||
import org.thingsboard.server.dao.queue.QueueService;
|
||||
import org.thingsboard.server.dao.queue.QueueStatsService;
|
||||
import org.thingsboard.server.dao.relation.RelationService;
|
||||
import org.thingsboard.server.dao.resource.TbResourceDataCache;
|
||||
import org.thingsboard.server.dao.resource.ResourceService;
|
||||
import org.thingsboard.server.dao.rule.RuleChainService;
|
||||
import org.thingsboard.server.dao.rule.RuleNodeStateService;
|
||||
@ -511,6 +512,10 @@ public class ActorSystemContext {
|
||||
@Getter
|
||||
private ResourceService resourceService;
|
||||
|
||||
@Autowired
|
||||
@Getter
|
||||
private TbResourceDataCache resourceDataCache;
|
||||
|
||||
@Lazy
|
||||
@Autowired(required = false)
|
||||
@Getter
|
||||
|
||||
@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.Device;
|
||||
import org.thingsboard.server.common.data.DeviceProfile;
|
||||
import org.thingsboard.server.common.data.EntityType;
|
||||
import org.thingsboard.server.common.data.HasRuleEngineProfile;
|
||||
import org.thingsboard.server.common.data.HasTenantId;
|
||||
import org.thingsboard.server.common.data.StringUtils;
|
||||
import org.thingsboard.server.common.data.TenantProfile;
|
||||
import org.thingsboard.server.common.data.alarm.Alarm;
|
||||
@ -60,6 +61,7 @@ import org.thingsboard.server.common.data.id.AssetId;
|
||||
import org.thingsboard.server.common.data.id.CustomerId;
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
import org.thingsboard.server.common.data.id.EntityId;
|
||||
import org.thingsboard.server.common.data.id.HasId;
|
||||
import org.thingsboard.server.common.data.id.RuleChainId;
|
||||
import org.thingsboard.server.common.data.id.RuleNodeId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
@ -110,6 +112,7 @@ import org.thingsboard.server.dao.ota.OtaPackageService;
|
||||
import org.thingsboard.server.dao.queue.QueueService;
|
||||
import org.thingsboard.server.dao.queue.QueueStatsService;
|
||||
import org.thingsboard.server.dao.relation.RelationService;
|
||||
import org.thingsboard.server.dao.resource.TbResourceDataCache;
|
||||
import org.thingsboard.server.dao.resource.ResourceService;
|
||||
import org.thingsboard.server.dao.rule.RuleChainService;
|
||||
import org.thingsboard.server.dao.tenant.TenantService;
|
||||
@ -770,6 +773,11 @@ public class DefaultTbContext implements TbContext {
|
||||
return mainCtx.getResourceService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TbResourceDataCache getTbResourceDataCache() {
|
||||
return mainCtx.getResourceDataCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OtaPackageService getOtaPackageService() {
|
||||
return mainCtx.getOtaPackageService();
|
||||
@ -1054,7 +1062,18 @@ public class DefaultTbContext implements TbContext {
|
||||
|
||||
@Override
|
||||
public void checkTenantEntity(EntityId entityId) throws TbNodeException {
|
||||
if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) {
|
||||
TenantId actualTenantId = TenantIdLoader.findTenantId(this, entityId);
|
||||
assertSameTenantId(actualTenantId, entityId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E extends HasId<I> & HasTenantId, I extends EntityId> void checkTenantEntity(E entity) throws TbNodeException {
|
||||
TenantId actualTenantId = entity.getTenantId();
|
||||
assertSameTenantId(actualTenantId, entity.getId());
|
||||
}
|
||||
|
||||
private void assertSameTenantId(TenantId tenantId, EntityId entityId) throws TbNodeException {
|
||||
if (!getTenantId().equals(tenantId)) {
|
||||
throw new TbNodeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.", true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,14 +55,17 @@ import org.thingsboard.server.common.data.util.ThrowingSupplier;
|
||||
import org.thingsboard.server.config.annotations.ApiOperation;
|
||||
import org.thingsboard.server.queue.util.TbCoreComponent;
|
||||
import org.thingsboard.server.service.resource.TbResourceService;
|
||||
import org.thingsboard.server.service.security.model.SecurityUser;
|
||||
import org.thingsboard.server.service.security.permission.Operation;
|
||||
import org.thingsboard.server.service.security.permission.Resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
|
||||
import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION;
|
||||
@ -263,6 +266,20 @@ public class TbResourceController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIds)")
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
|
||||
@GetMapping(value = "/resource", params = {"resourceIds"})
|
||||
public List<TbResourceInfo> getSystemOrTenantResourcesByIds(
|
||||
@Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
|
||||
@RequestParam("resourceIds") Set<UUID> resourceUuids) throws ThingsboardException {
|
||||
SecurityUser user = getCurrentUser();
|
||||
List<TbResourceId> resourceIds = new ArrayList<>();
|
||||
for (UUID resourceId : resourceUuids) {
|
||||
resourceIds.add(new TbResourceId(resourceId));
|
||||
}
|
||||
return resourceService.findSystemOrTenantResourcesByIds(user.getTenantId(), resourceIds);
|
||||
}
|
||||
|
||||
@ApiOperation(value = "Get All Resource Infos (getAllResources)",
|
||||
notes = "Returns a page of Resource Info objects owned by tenant. " +
|
||||
PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
|
||||
|
||||
@ -35,6 +35,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
|
||||
import org.thingsboard.server.common.msg.queue.ServiceType;
|
||||
import org.thingsboard.server.common.msg.queue.TbCallback;
|
||||
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
|
||||
import org.thingsboard.server.dao.resource.TbResourceDataCache;
|
||||
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
|
||||
@ -83,6 +84,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
|
||||
ActorSystemContext actorContext,
|
||||
TbDeviceProfileCache deviceProfileCache,
|
||||
TbAssetProfileCache assetProfileCache,
|
||||
TbResourceDataCache tbResourceDataCache,
|
||||
TbTenantProfileCache tenantProfileCache,
|
||||
TbApiUsageStateService apiUsageStateService,
|
||||
PartitionService partitionService,
|
||||
@ -90,7 +92,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
|
||||
JwtSettingsService jwtSettingsService,
|
||||
CalculatedFieldCache calculatedFieldCache,
|
||||
CalculatedFieldStateService stateService) {
|
||||
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService,
|
||||
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
|
||||
eventPublisher, jwtSettingsService);
|
||||
this.queueFactory = tbQueueFactory;
|
||||
this.stateService = stateService;
|
||||
|
||||
@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
|
||||
import org.thingsboard.server.common.data.id.EdgeId;
|
||||
import org.thingsboard.server.common.data.id.EntityId;
|
||||
import org.thingsboard.server.common.data.id.RuleChainId;
|
||||
import org.thingsboard.server.common.data.id.TbResourceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.msg.TbMsgType;
|
||||
import org.thingsboard.server.common.data.page.PageData;
|
||||
@ -435,8 +436,9 @@ public class DefaultTbClusterService implements TbClusterService {
|
||||
|
||||
@Override
|
||||
public void onResourceChange(TbResourceInfo resource, TbQueueCallback callback) {
|
||||
TenantId tenantId = resource.getTenantId();
|
||||
TbResourceId resourceId = resource.getId();
|
||||
if (resource.getResourceType() == ResourceType.LWM2M_MODEL) {
|
||||
TenantId tenantId = resource.getTenantId();
|
||||
log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey());
|
||||
ResourceUpdateMsg resourceUpdateMsg = ResourceUpdateMsg.newBuilder()
|
||||
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
|
||||
@ -447,6 +449,7 @@ public class DefaultTbClusterService implements TbClusterService {
|
||||
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build();
|
||||
broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback);
|
||||
}
|
||||
broadcastEntityStateChangeEvent(tenantId, resourceId, ComponentLifecycleEvent.UPDATED);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -462,6 +465,7 @@ public class DefaultTbClusterService implements TbClusterService {
|
||||
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceDeleteMsg).build();
|
||||
broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback);
|
||||
}
|
||||
broadcastEntityStateChangeEvent(resource.getTenantId(), resource.getId(), ComponentLifecycleEvent.DELETED);
|
||||
}
|
||||
|
||||
private <T> void broadcastEntityChangeToTransport(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) {
|
||||
@ -592,7 +596,8 @@ public class DefaultTbClusterService implements TbClusterService {
|
||||
EntityType.TENANT_PROFILE,
|
||||
EntityType.DEVICE_PROFILE,
|
||||
EntityType.ASSET_PROFILE,
|
||||
EntityType.JOB)
|
||||
EntityType.JOB,
|
||||
EntityType.TB_RESOURCE)
|
||||
|| (entityType == EntityType.ASSET && msg.getEvent() == ComponentLifecycleEvent.UPDATED)
|
||||
|| (entityType == EntityType.DEVICE && msg.getEvent() == ComponentLifecycleEvent.UPDATED)
|
||||
) {
|
||||
|
||||
@ -55,6 +55,7 @@ import org.thingsboard.server.common.stats.StatsFactory;
|
||||
import org.thingsboard.server.common.util.KvProtoUtil;
|
||||
import org.thingsboard.server.common.util.ProtoUtils;
|
||||
import org.thingsboard.server.dao.resource.ImageCacheKey;
|
||||
import org.thingsboard.server.dao.resource.TbResourceDataCache;
|
||||
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto;
|
||||
@ -176,10 +177,11 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
|
||||
NotificationSchedulerService notificationSchedulerService,
|
||||
NotificationRuleProcessor notificationRuleProcessor,
|
||||
TbImageService imageService,
|
||||
TbResourceDataCache tbResourceDataCache,
|
||||
RuleEngineCallService ruleEngineCallService,
|
||||
CalculatedFieldCache calculatedFieldCache,
|
||||
EdqsService edqsService) {
|
||||
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService,
|
||||
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
|
||||
eventPublisher, jwtSettingsService);
|
||||
this.stateService = stateService;
|
||||
this.localSubscriptionService = localSubscriptionService;
|
||||
|
||||
@ -87,7 +87,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService<ToEdge
|
||||
|
||||
public DefaultTbEdgeConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext,
|
||||
StatsFactory statsFactory, EdgeContextComponent edgeCtx) {
|
||||
super(actorContext, null, null, null, null, null, null,
|
||||
super(actorContext, null, null, null, null, null, null, null,
|
||||
null, null);
|
||||
this.edgeCtx = edgeCtx;
|
||||
this.stats = new EdgeConsumerStats(statsFactory);
|
||||
|
||||
@ -33,6 +33,7 @@ import org.thingsboard.server.common.msg.queue.TbCallback;
|
||||
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
|
||||
import org.thingsboard.server.common.util.ProtoUtils;
|
||||
import org.thingsboard.server.dao.queue.QueueService;
|
||||
import org.thingsboard.server.dao.resource.TbResourceDataCache;
|
||||
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||
import org.thingsboard.server.gen.transport.TransportProtos.QueueDeleteMsg;
|
||||
@ -78,13 +79,14 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo
|
||||
QueueService queueService,
|
||||
TbDeviceProfileCache deviceProfileCache,
|
||||
TbAssetProfileCache assetProfileCache,
|
||||
TbResourceDataCache tbResourceDataCache,
|
||||
TbTenantProfileCache tenantProfileCache,
|
||||
TbApiUsageStateService apiUsageStateService,
|
||||
PartitionService partitionService,
|
||||
ApplicationEventPublisher eventPublisher,
|
||||
JwtSettingsService jwtSettingsService,
|
||||
CalculatedFieldCache calculatedFieldCache) {
|
||||
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
|
||||
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
|
||||
this.ctx = ctx;
|
||||
this.tbDeviceRpcService = tbDeviceRpcService;
|
||||
this.queueService = queueService;
|
||||
|
||||
@ -30,12 +30,14 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId;
|
||||
import org.thingsboard.server.common.data.id.CustomerId;
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
import org.thingsboard.server.common.data.id.DeviceProfileId;
|
||||
import org.thingsboard.server.common.data.id.TbResourceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.id.TenantProfileId;
|
||||
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
|
||||
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
|
||||
import org.thingsboard.server.common.msg.queue.ServiceType;
|
||||
import org.thingsboard.server.common.msg.queue.TbCallback;
|
||||
import org.thingsboard.server.dao.resource.TbResourceDataCache;
|
||||
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
|
||||
import org.thingsboard.server.queue.TbQueueConsumer;
|
||||
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
|
||||
@ -72,6 +74,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
|
||||
protected final TbTenantProfileCache tenantProfileCache;
|
||||
protected final TbDeviceProfileCache deviceProfileCache;
|
||||
protected final TbAssetProfileCache assetProfileCache;
|
||||
protected final TbResourceDataCache tbResourceDataCache;
|
||||
protected final CalculatedFieldCache calculatedFieldCache;
|
||||
protected final TbApiUsageStateService apiUsageStateService;
|
||||
protected final PartitionService partitionService;
|
||||
@ -202,6 +205,8 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
|
||||
} else {
|
||||
calculatedFieldCache.evict((CalculatedFieldId) componentLifecycleMsg.getEntityId());
|
||||
}
|
||||
} else if (EntityType.TB_RESOURCE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
|
||||
tbResourceDataCache.evictResourceData(tenantId, new TbResourceId(componentLifecycleMsg.getEntityId().getId()));
|
||||
}
|
||||
|
||||
eventPublisher.publishEvent(componentLifecycleMsg);
|
||||
|
||||
@ -18,6 +18,7 @@ package org.thingsboard.server.service.queue.processing;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.thingsboard.server.actors.ActorSystemContext;
|
||||
import org.thingsboard.server.dao.resource.TbResourceDataCache;
|
||||
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
|
||||
import org.thingsboard.server.queue.discovery.PartitionService;
|
||||
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
|
||||
@ -43,12 +44,13 @@ public abstract class AbstractPartitionBasedConsumerService<N extends com.google
|
||||
TbTenantProfileCache tenantProfileCache,
|
||||
TbDeviceProfileCache deviceProfileCache,
|
||||
TbAssetProfileCache assetProfileCache,
|
||||
TbResourceDataCache tbResourceDataCache,
|
||||
CalculatedFieldCache calculatedFieldCache,
|
||||
TbApiUsageStateService apiUsageStateService,
|
||||
PartitionService partitionService,
|
||||
ApplicationEventPublisher eventPublisher,
|
||||
JwtSettingsService jwtSettingsService) {
|
||||
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
|
||||
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
|
||||
@ -102,7 +102,6 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
|
||||
if (result.isSuccess()) {
|
||||
logEntityActionService.logEntityAction(tenantId, resourceId, tbResource, actionType, user, resourceId.toString());
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE),
|
||||
|
||||
@ -676,6 +676,9 @@ cache:
|
||||
maxSize: "${CACHE_SPECS_IMAGE_ETAGS_MAX_SIZE:10000}" # 0 means the cache is disabled
|
||||
systemImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_SYSTEM_BROWSER_TTL:0}" # Browser cache TTL for system images in minutes. 0 means the cache is disabled
|
||||
tenantImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_TENANT_BROWSER_TTL:0}" # Browser cache TTL for tenant images in minutes. 0 means the cache is disabled
|
||||
tbResourceData:
|
||||
timeToLiveInMinutes: "${CACHE_SPECS_RESOURCE_DATA_TTL:10080}" # TB resource data cache TTL
|
||||
maxSize: "${CACHE_SPECS_RESOURCE_DATA_MAX_SIZE:100000}" # 0 means the cache is disabled
|
||||
|
||||
# Spring data parameters
|
||||
spring.data.redis.repositories.enabled: false # Disable this because it is not required.
|
||||
|
||||
@ -31,6 +31,7 @@ import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.server.common.data.Dashboard;
|
||||
import org.thingsboard.server.common.data.DashboardInfo;
|
||||
import org.thingsboard.server.common.data.Device;
|
||||
import org.thingsboard.server.common.data.EntityInfo;
|
||||
import org.thingsboard.server.common.data.EntityType;
|
||||
import org.thingsboard.server.common.data.ResourceType;
|
||||
import org.thingsboard.server.common.data.StringUtils;
|
||||
@ -321,18 +322,15 @@ public class TbResourceControllerTest extends AbstractControllerTest {
|
||||
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
|
||||
Assert.assertNotNull(referenceValues);
|
||||
|
||||
var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<WidgetTypeInfo>>>() {
|
||||
var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<EntityInfo>>>() {
|
||||
});
|
||||
Assert.assertNotNull(widgetTypeInfos);
|
||||
Assert.assertFalse(widgetTypeInfos.isEmpty());
|
||||
Assert.assertEquals(1, widgetTypeInfos.size());
|
||||
|
||||
var dashboardInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0);
|
||||
Assert.assertNotNull(dashboardInfo);
|
||||
|
||||
WidgetTypeInfo foundedWidgetType = doGet("/api/widgetTypeInfo/" + savedWidgetType.getId().getId().toString(), WidgetTypeInfo.class);
|
||||
Assert.assertNotNull(foundedWidgetType);
|
||||
Assert.assertEquals(foundedWidgetType, dashboardInfo);
|
||||
var widgetTypeInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0);
|
||||
Assert.assertNotNull(widgetTypeInfo);
|
||||
Assert.assertEquals(new EntityInfo(savedWidgetType.getId(), savedWidgetType.getName()), widgetTypeInfo);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -372,7 +370,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
|
||||
Assert.assertTrue(isSuccess);
|
||||
|
||||
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
|
||||
var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<WidgetTypeInfo>>>() {
|
||||
var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<EntityInfo>>>() {
|
||||
});
|
||||
Assert.assertNull(widgetTypeInfos);
|
||||
}
|
||||
@ -417,7 +415,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
|
||||
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
|
||||
Assert.assertNotNull(referenceValues);
|
||||
|
||||
var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<DashboardInfo>>>() {
|
||||
var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<EntityInfo>>>() {
|
||||
});
|
||||
Assert.assertNotNull(dashboardInfos);
|
||||
Assert.assertFalse(dashboardInfos.isEmpty());
|
||||
@ -425,10 +423,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
|
||||
|
||||
var dashboardInfo = dashboardInfos.get(EntityType.DASHBOARD.name()).get(0);
|
||||
Assert.assertNotNull(dashboardInfo);
|
||||
|
||||
DashboardInfo foundDashboard = doGet("/api/dashboard/info/" + savedDashboard.getId().getId().toString(), DashboardInfo.class);
|
||||
Assert.assertNotNull(foundDashboard);
|
||||
Assert.assertEquals(foundDashboard, dashboardInfo);
|
||||
Assert.assertEquals(new EntityInfo(savedDashboard.getId(), savedDashboard.getName()), dashboardInfo);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -469,7 +464,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
|
||||
Assert.assertTrue(isSuccess);
|
||||
|
||||
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
|
||||
var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<DashboardInfo>>>() {
|
||||
var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<EntityInfo>>>() {
|
||||
});
|
||||
Assert.assertNull(dashboardInfos);
|
||||
}
|
||||
|
||||
@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 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.resource;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.server.common.data.GeneralFileDescriptor;
|
||||
import org.thingsboard.server.common.data.ResourceType;
|
||||
import org.thingsboard.server.common.data.TbResource;
|
||||
import org.thingsboard.server.common.data.TbResourceDataInfo;
|
||||
import org.thingsboard.server.common.data.TbResourceInfo;
|
||||
import org.thingsboard.server.controller.AbstractControllerTest;
|
||||
import org.thingsboard.server.dao.resource.ResourceService;
|
||||
import org.thingsboard.server.dao.resource.TbResourceDataCache;
|
||||
import org.thingsboard.server.dao.service.DaoSqlTest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.timeout;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
@DaoSqlTest
|
||||
public class DefaultResourceDataCacheTest extends AbstractControllerTest {
|
||||
|
||||
@MockitoSpyBean
|
||||
private ResourceService resourceService;
|
||||
@Autowired
|
||||
private TbResourceService tbResourceService;
|
||||
@MockitoSpyBean
|
||||
private TbResourceDataCache resourceDataCache;
|
||||
|
||||
@Test
|
||||
public void testGetCachedResourceData() throws Exception {
|
||||
loginTenantAdmin();
|
||||
|
||||
TbResource resource = new TbResource();
|
||||
resource.setTenantId(tenantId);
|
||||
resource.setTitle("File for AI request");
|
||||
resource.setResourceType(ResourceType.GENERAL);
|
||||
resource.setFileName("myTestJson.json");
|
||||
GeneralFileDescriptor descriptor = new GeneralFileDescriptor("application/json");
|
||||
resource.setDescriptorValue(descriptor);
|
||||
byte[] data = "This is a test prompt for AI request.".getBytes();
|
||||
resource.setData(data);
|
||||
TbResourceInfo savedResource = tbResourceService.save(resource);
|
||||
verify(resourceDataCache, timeout(2000).times(1)).evictResourceData(tenantId, savedResource.getId());
|
||||
|
||||
TbResourceDataInfo cachedData = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get();
|
||||
assertThat(cachedData.getData()).isEqualTo(data);
|
||||
assertThat(JacksonUtil.treeToValue(cachedData.getDescriptor(), GeneralFileDescriptor.class)).isEqualTo(descriptor);
|
||||
verify(resourceService).getResourceDataInfo(tenantId, savedResource.getId());
|
||||
|
||||
// retrieve resource data second time
|
||||
clearInvocations(resourceService);
|
||||
TbResourceDataInfo cachedData2 = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get();
|
||||
assertThat(cachedData2.getData()).isEqualTo(data);
|
||||
verifyNoMoreInteractions(resourceService);
|
||||
|
||||
// delete resource, check cache
|
||||
TbResource resourceById = resourceService.findResourceById(tenantId, savedResource.getId());
|
||||
tbResourceService.delete(resourceById, true, null);
|
||||
verify(resourceDataCache, timeout(2000).times(2)).evictResourceData(tenantId, savedResource.getId());
|
||||
TbResourceDataInfo cachedDataAfterDeletion = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get();
|
||||
assertThat(cachedDataAfterDeletion).isEqualTo(null);
|
||||
}
|
||||
|
||||
}
|
||||
@ -17,6 +17,7 @@ package org.thingsboard.server.service.resource.sql;
|
||||
|
||||
import com.datastax.oss.driver.api.core.uuid.Uuids;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.node.TextNode;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
@ -24,8 +25,10 @@ import org.junit.Test;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.rule.engine.ai.TbAiNode;
|
||||
import org.thingsboard.rule.engine.ai.TbAiNodeConfiguration;
|
||||
import org.thingsboard.rule.engine.ai.TbResponseFormat;
|
||||
import org.thingsboard.server.common.data.Dashboard;
|
||||
import org.thingsboard.server.common.data.DashboardInfo;
|
||||
import org.thingsboard.server.common.data.DataConstants;
|
||||
import org.thingsboard.server.common.data.EntityInfo;
|
||||
import org.thingsboard.server.common.data.EntityType;
|
||||
@ -37,26 +40,40 @@ import org.thingsboard.server.common.data.TbResourceInfoFilter;
|
||||
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.ai.model.chat.OpenAiChatModelConfig;
|
||||
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
|
||||
import org.thingsboard.server.common.data.debug.DebugSettings;
|
||||
import org.thingsboard.server.common.data.exception.ThingsboardException;
|
||||
import org.thingsboard.server.common.data.id.RuleChainId;
|
||||
import org.thingsboard.server.common.data.id.TbResourceId;
|
||||
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.rule.RuleChain;
|
||||
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
|
||||
import org.thingsboard.server.common.data.rule.RuleChainType;
|
||||
import org.thingsboard.server.common.data.rule.RuleNode;
|
||||
import org.thingsboard.server.common.data.security.Authority;
|
||||
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
|
||||
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
|
||||
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
|
||||
import org.thingsboard.server.controller.AbstractControllerTest;
|
||||
import org.thingsboard.server.dao.ai.AiModelService;
|
||||
import org.thingsboard.server.dao.dashboard.DashboardService;
|
||||
import org.thingsboard.server.dao.exception.DataValidationException;
|
||||
import org.thingsboard.server.dao.resource.ResourceService;
|
||||
import org.thingsboard.server.dao.rule.RuleChainService;
|
||||
import org.thingsboard.server.dao.service.DaoSqlTest;
|
||||
import org.thingsboard.server.dao.widget.WidgetTypeService;
|
||||
import org.thingsboard.server.service.resource.TbResourceService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@ -135,6 +152,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
|
||||
private WidgetTypeService widgetTypeService;
|
||||
@Autowired
|
||||
private DashboardService dashboardService;
|
||||
@Autowired
|
||||
private RuleChainService ruleChainService;
|
||||
@Autowired
|
||||
private AiModelService aiModelService;
|
||||
|
||||
private Tenant savedTenant;
|
||||
private User tenantAdmin;
|
||||
@ -453,11 +474,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
|
||||
Assert.assertFalse(result.getReferences().isEmpty());
|
||||
Assert.assertEquals(1, result.getReferences().size());
|
||||
|
||||
WidgetTypeInfo widgetTypeInfo = (WidgetTypeInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0);
|
||||
WidgetTypeInfo foundWidgetTypeInfo = new WidgetTypeInfo(foundWidgetType);
|
||||
EntityInfo widgetTypeInfo = (EntityInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0);
|
||||
Assert.assertNotNull(widgetTypeInfo);
|
||||
Assert.assertNotNull(foundWidgetTypeInfo);
|
||||
Assert.assertEquals(widgetTypeInfo, foundWidgetTypeInfo);
|
||||
Assert.assertEquals(widgetTypeInfo, new EntityInfo(foundWidgetType.getId(), foundWidgetType.getName()));
|
||||
|
||||
TbResourceInfo foundResourceInfo = resourceService.findResourceInfoById(savedTenant.getId(), savedResource.getId());
|
||||
Assert.assertNotNull(foundResource);
|
||||
@ -546,11 +565,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
|
||||
Assert.assertNotNull(result.getReferences());
|
||||
Assert.assertEquals(1, result.getReferences().size());
|
||||
|
||||
DashboardInfo dashboardInfo = (DashboardInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0);
|
||||
DashboardInfo foundDashboardInfo = dashboardService.findDashboardInfoById(savedTenant.getId(), savedDashboard.getId());
|
||||
EntityInfo dashboardInfo = (EntityInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0);
|
||||
Assert.assertNotNull(dashboardInfo);
|
||||
Assert.assertNotNull(foundDashboardInfo);
|
||||
Assert.assertEquals(foundDashboardInfo, dashboardInfo);
|
||||
Assert.assertEquals(new EntityInfo(savedDashboard.getId(), savedDashboard.getName()), dashboardInfo);
|
||||
|
||||
foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId());
|
||||
Assert.assertNotNull(foundResource);
|
||||
@ -598,6 +615,90 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
|
||||
Assert.assertNull(foundResource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldNotDeleteResourceIfUsedInAiNode() throws Exception {
|
||||
TbResource resource = new TbResource();
|
||||
resource.setResourceType(ResourceType.GENERAL);
|
||||
resource.setTitle("My resource");
|
||||
resource.setFileName("test.json");
|
||||
resource.setTenantId(savedTenant.getId());
|
||||
resource.setData("<test></test>".getBytes());
|
||||
TbResourceInfo savedResource = tbResourceService.save(resource);
|
||||
RuleChainMetaData ruleChain = createRuleChainReferringResource(savedResource.getId());
|
||||
|
||||
TbResourceDeleteResult result = tbResourceService.delete(savedResource, false, null);
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.isSuccess()).isFalse();
|
||||
assertThat(result.getReferences()).isNotEmpty().hasSize(1);
|
||||
EntityInfo entityInfo = (EntityInfo) result.getReferences().get(EntityType.RULE_CHAIN.name()).get(0);
|
||||
assertThat(entityInfo).isEqualTo(new EntityInfo(ruleChain.getRuleChainId(), "Test"));
|
||||
|
||||
TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId());
|
||||
assertThat(foundResource).isNotNull();
|
||||
|
||||
// force delete
|
||||
TbResourceDeleteResult deleteResult = tbResourceService.delete(savedResource, true, null);
|
||||
assertThat(deleteResult).isNotNull();
|
||||
assertThat(deleteResult.isSuccess()).isTrue();
|
||||
|
||||
TbResource resourceAfterDeletion = resourceService.findResourceById(savedTenant.getId(), savedResource.getId());
|
||||
assertThat(resourceAfterDeletion).isNull();
|
||||
}
|
||||
|
||||
private RuleChainMetaData createRuleChainReferringResource(TbResourceId resourceId) {
|
||||
AiModel model = constructValidOpenAiModel("Test model");
|
||||
AiModel saved = aiModelService.save(model);
|
||||
|
||||
RuleChain ruleChain = new RuleChain();
|
||||
ruleChain.setTenantId(tenantId);
|
||||
ruleChain.setName("Test");
|
||||
ruleChain.setType(RuleChainType.CORE);
|
||||
ruleChain.setDebugMode(true);
|
||||
ruleChain.setConfiguration(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
|
||||
ruleChain = ruleChainService.saveRuleChain(ruleChain);
|
||||
RuleChainId ruleChainId = ruleChain.getId();
|
||||
|
||||
RuleChainMetaData metaData = new RuleChainMetaData();
|
||||
metaData.setRuleChainId(ruleChainId);
|
||||
|
||||
RuleNode aiNode = new RuleNode();
|
||||
aiNode.setName("Ai request");
|
||||
aiNode.setType(org.thingsboard.rule.engine.ai.TbAiNode.class.getName());
|
||||
aiNode.setConfigurationVersion(TbAiNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
|
||||
aiNode.setDebugSettings(DebugSettings.all());
|
||||
TbAiNodeConfiguration configuration = new TbAiNodeConfiguration();
|
||||
configuration.setResourceIds(Set.of(resourceId.getId()));
|
||||
configuration.setModelId(saved.getId());
|
||||
configuration.setResponseFormat(new TbResponseFormat.TbJsonResponseFormat());
|
||||
configuration.setTimeoutSeconds(1);
|
||||
configuration.setUserPrompt("What is temp");
|
||||
aiNode.setConfiguration(JacksonUtil.valueToTree(configuration));
|
||||
|
||||
metaData.setNodes(Arrays.asList(aiNode));
|
||||
metaData.setFirstNodeIndex(0);
|
||||
ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity());
|
||||
return ruleChainService.loadRuleChainMetaData(tenantId, ruleChainId);
|
||||
}
|
||||
|
||||
private AiModel constructValidOpenAiModel(String name) {
|
||||
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(name)
|
||||
.configuration(modelConfig)
|
||||
.build();
|
||||
}
|
||||
@Test
|
||||
public void testFindTenantResourcesByTenantId() throws Exception {
|
||||
loginSysAdmin();
|
||||
|
||||
@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.ResourceExportData;
|
||||
import org.thingsboard.server.common.data.ResourceSubType;
|
||||
import org.thingsboard.server.common.data.ResourceType;
|
||||
import org.thingsboard.server.common.data.TbResource;
|
||||
import org.thingsboard.server.common.data.TbResourceDataInfo;
|
||||
import org.thingsboard.server.common.data.TbResourceDeleteResult;
|
||||
import org.thingsboard.server.common.data.TbResourceInfo;
|
||||
import org.thingsboard.server.common.data.TbResourceInfoFilter;
|
||||
@ -46,6 +47,8 @@ public interface ResourceService extends EntityDaoService {
|
||||
|
||||
byte[] getResourceData(TenantId tenantId, TbResourceId resourceId);
|
||||
|
||||
TbResourceDataInfo getResourceDataInfo(TenantId tenantId, TbResourceId resourceId);
|
||||
|
||||
ResourceExportData exportResource(TbResourceInfo resourceInfo);
|
||||
|
||||
List<ResourceExportData> exportResources(TenantId tenantId, Collection<TbResourceInfo> resources);
|
||||
@ -90,4 +93,6 @@ public interface ResourceService extends EntityDaoService {
|
||||
|
||||
TbResource createOrUpdateSystemResource(ResourceType resourceType, ResourceSubType resourceSubType, String resourceKey, byte[] data);
|
||||
|
||||
List<TbResourceInfo> findSystemOrTenantResourcesByIds(TenantId tenantId, List<TbResourceId> resourceIds);
|
||||
|
||||
}
|
||||
|
||||
@ -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.dao.resource;
|
||||
|
||||
import com.google.common.util.concurrent.FluentFuture;
|
||||
import org.thingsboard.server.common.data.TbResourceDataInfo;
|
||||
import org.thingsboard.server.common.data.id.TbResourceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
|
||||
public interface TbResourceDataCache {
|
||||
|
||||
FluentFuture<TbResourceDataInfo> getResourceDataInfoAsync(TenantId tenantId, TbResourceId resourceId);
|
||||
|
||||
void evictResourceData(TenantId tenantId, TbResourceId resourceId);
|
||||
}
|
||||
@ -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.common.data;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class GeneralFileDescriptor {
|
||||
private String mediaType;
|
||||
}
|
||||
@ -25,7 +25,8 @@ public enum ResourceType {
|
||||
PKCS_12("application/x-pkcs12", false, false),
|
||||
JS_MODULE("application/javascript", true, true),
|
||||
IMAGE(null, true, true),
|
||||
DASHBOARD("application/json", true, true);
|
||||
DASHBOARD("application/json", true, true),
|
||||
GENERAL(null, false, true);
|
||||
|
||||
@Getter
|
||||
private final String mediaType;
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
package org.thingsboard.server.common.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonSetter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
@ -86,6 +87,11 @@ public class TbResource extends TbResourceInfo {
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public TbResourceDataInfo toResourceDataInfo() {
|
||||
return new TbResourceDataInfo(data, getDescriptor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString();
|
||||
|
||||
@ -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;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class TbResourceDataInfo {
|
||||
|
||||
private byte[] data;
|
||||
private JsonNode descriptor;
|
||||
|
||||
}
|
||||
@ -17,7 +17,6 @@ package org.thingsboard.server.common.data;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import org.thingsboard.server.common.data.id.HasId;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -27,6 +26,6 @@ import java.util.Map;
|
||||
public class TbResourceDeleteResult {
|
||||
|
||||
private boolean success;
|
||||
private Map<String, List<? extends HasId<?>>> references;
|
||||
private Map<String, List<EntityInfo>> references;
|
||||
|
||||
}
|
||||
|
||||
@ -15,12 +15,15 @@
|
||||
*/
|
||||
package org.thingsboard.common.util;
|
||||
|
||||
import com.google.common.util.concurrent.FluentFuture;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@ -71,4 +74,16 @@ public class DonAsynchron {
|
||||
return future;
|
||||
}
|
||||
|
||||
public static <T> FluentFuture<T> toFluentFuture(CompletableFuture<T> completable) {
|
||||
SettableFuture<T> future = SettableFuture.create();
|
||||
completable.whenComplete((result, exception) -> {
|
||||
if (exception != null) {
|
||||
future.setException(exception);
|
||||
} else {
|
||||
future.set(result);
|
||||
}
|
||||
});
|
||||
return FluentFuture.from(future);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.thingsboard.server.dao;
|
||||
|
||||
import org.thingsboard.server.common.data.EntityInfo;
|
||||
import org.thingsboard.server.common.data.id.HasId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
|
||||
@ -22,8 +23,8 @@ import java.util.List;
|
||||
|
||||
public interface ResourceContainerDao<T extends HasId<?>> {
|
||||
|
||||
List<T> findByTenantIdAndResourceLink(TenantId tenantId, String link, int limit);
|
||||
List<EntityInfo> findByTenantIdAndResource(TenantId tenantId, String reference, int limit);
|
||||
|
||||
List<T> findByResourceLink(String link, int limit);
|
||||
List<EntityInfo> findByResource(String reference, int limit);
|
||||
|
||||
}
|
||||
|
||||
@ -50,6 +50,7 @@ import org.thingsboard.server.dao.ImageContainerDao;
|
||||
import org.thingsboard.server.dao.asset.AssetProfileDao;
|
||||
import org.thingsboard.server.dao.dashboard.DashboardInfoDao;
|
||||
import org.thingsboard.server.dao.device.DeviceProfileDao;
|
||||
import org.thingsboard.server.dao.rule.RuleChainDao;
|
||||
import org.thingsboard.server.dao.service.Validator;
|
||||
import org.thingsboard.server.dao.service.validator.ResourceDataValidator;
|
||||
import org.thingsboard.server.dao.util.ImageUtils;
|
||||
@ -109,8 +110,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic
|
||||
|
||||
public BaseImageService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, ResourceDataValidator resourceValidator,
|
||||
AssetProfileDao assetProfileDao, DeviceProfileDao deviceProfileDao, WidgetsBundleDao widgetsBundleDao,
|
||||
WidgetTypeDao widgetTypeDao, DashboardInfoDao dashboardInfoDao) {
|
||||
super(resourceDao, resourceInfoDao, resourceValidator, widgetTypeDao, dashboardInfoDao);
|
||||
WidgetTypeDao widgetTypeDao, DashboardInfoDao dashboardInfoDao, RuleChainDao ruleChainDao) {
|
||||
super(resourceDao, resourceInfoDao, resourceValidator, widgetTypeDao, dashboardInfoDao, ruleChainDao);
|
||||
this.assetProfileDao = assetProfileDao;
|
||||
this.deviceProfileDao = deviceProfileDao;
|
||||
this.widgetsBundleDao = widgetsBundleDao;
|
||||
|
||||
@ -35,11 +35,13 @@ import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey;
|
||||
import org.thingsboard.server.cache.resourceInfo.ResourceInfoEvictEvent;
|
||||
import org.thingsboard.server.common.data.Dashboard;
|
||||
import org.thingsboard.server.common.data.DataConstants;
|
||||
import org.thingsboard.server.common.data.EntityInfo;
|
||||
import org.thingsboard.server.common.data.EntityType;
|
||||
import org.thingsboard.server.common.data.ResourceExportData;
|
||||
import org.thingsboard.server.common.data.ResourceSubType;
|
||||
import org.thingsboard.server.common.data.ResourceType;
|
||||
import org.thingsboard.server.common.data.TbResource;
|
||||
import org.thingsboard.server.common.data.TbResourceDataInfo;
|
||||
import org.thingsboard.server.common.data.TbResourceDeleteResult;
|
||||
import org.thingsboard.server.common.data.TbResourceInfo;
|
||||
import org.thingsboard.server.common.data.TbResourceInfoFilter;
|
||||
@ -56,6 +58,7 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
|
||||
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
|
||||
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
|
||||
import org.thingsboard.server.dao.exception.DataValidationException;
|
||||
import org.thingsboard.server.dao.rule.RuleChainDao;
|
||||
import org.thingsboard.server.dao.service.PaginatedRemover;
|
||||
import org.thingsboard.server.dao.service.Validator;
|
||||
import org.thingsboard.server.dao.service.validator.ResourceDataValidator;
|
||||
@ -92,13 +95,16 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
|
||||
protected final ResourceDataValidator resourceValidator;
|
||||
protected final WidgetTypeDao widgetTypeDao;
|
||||
protected final DashboardInfoDao dashboardInfoDao;
|
||||
private final Map<EntityType, ResourceContainerDao<?>> resourceContainerDaoMap = new HashMap<>();
|
||||
protected final RuleChainDao ruleChainDao;
|
||||
private final Map<EntityType, ResourceContainerDao<?>> resourceLinkContainerDaoMap = new HashMap<>();
|
||||
private final Map<EntityType, ResourceContainerDao<?>> generalResourceContainerDaoMap = new HashMap<>();
|
||||
protected static final int MAX_ENTITIES_TO_FIND = 10;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
resourceContainerDaoMap.put(EntityType.WIDGET_TYPE, widgetTypeDao);
|
||||
resourceContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao);
|
||||
resourceLinkContainerDaoMap.put(EntityType.WIDGET_TYPE, widgetTypeDao);
|
||||
resourceLinkContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao);
|
||||
generalResourceContainerDaoMap.put(EntityType.RULE_CHAIN, ruleChainDao);
|
||||
}
|
||||
|
||||
@Autowired @Lazy
|
||||
@ -206,6 +212,12 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
|
||||
return resourceDao.getResourceData(tenantId, resourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TbResourceDataInfo getResourceDataInfo(TenantId tenantId, TbResourceId resourceId) {
|
||||
log.trace("Executing getResourceDataInfo [{}] [{}]", tenantId, resourceId);
|
||||
return resourceDao.getResourceDataInfo(tenantId, resourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceExportData exportResource(TbResourceInfo resourceInfo) {
|
||||
byte[] data = getResourceData(resourceInfo.getTenantId(), resourceInfo.getId());
|
||||
@ -344,32 +356,48 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
|
||||
}
|
||||
|
||||
if (!force) {
|
||||
if (resource.getResourceType() == ResourceType.JS_MODULE) {
|
||||
var link = resource.getLink();
|
||||
Map<String, List<? extends HasId<?>>> affectedEntities = new HashMap<>();
|
||||
|
||||
resourceContainerDaoMap.forEach((entityType, resourceContainerDao) -> {
|
||||
var entities = tenantId.isSysTenantId() ? resourceContainerDao.findByResourceLink(link, MAX_ENTITIES_TO_FIND) :
|
||||
resourceContainerDao.findByTenantIdAndResourceLink(tenantId, link, MAX_ENTITIES_TO_FIND);
|
||||
if (!entities.isEmpty()) {
|
||||
affectedEntities.put(entityType.name(), entities);
|
||||
}
|
||||
});
|
||||
|
||||
if (!affectedEntities.isEmpty()) {
|
||||
success = false;
|
||||
result.references(affectedEntities);
|
||||
}
|
||||
Map<String, List<EntityInfo>> references = findResourceReferences(tenantId, resource);
|
||||
if (!references.isEmpty()) {
|
||||
success = false;
|
||||
result.references(references);
|
||||
}
|
||||
}
|
||||
if (success) {
|
||||
resourceDao.removeById(tenantId, resourceId.getId());
|
||||
publishEvictEvent(new ResourceInfoEvictEvent(tenantId, resourceId));
|
||||
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entity(resource).entityId(resourceId).build());
|
||||
}
|
||||
|
||||
return result.success(success).build();
|
||||
}
|
||||
|
||||
private Map<String, List<EntityInfo>> findResourceReferences(TenantId tenantId, TbResourceInfo resource) {
|
||||
Map<String, List<EntityInfo>> references = new HashMap<>();
|
||||
|
||||
if (resource.getResourceType() == ResourceType.JS_MODULE) {
|
||||
var ref = resource.getLink();
|
||||
findReferences(tenantId, references, ref, resourceLinkContainerDaoMap);
|
||||
}
|
||||
|
||||
if (resource.getResourceType() == ResourceType.GENERAL) {
|
||||
var ref = resource.getId().getId().toString();
|
||||
findReferences(tenantId, references, ref, generalResourceContainerDaoMap);
|
||||
}
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
private void findReferences(TenantId tenantId, Map<String, List<EntityInfo>> references, String ref, Map<EntityType, ResourceContainerDao<?>> resourceLinkContainerDaoMap) {
|
||||
resourceLinkContainerDaoMap.forEach((entityType, dao) -> {
|
||||
List<EntityInfo> entities = tenantId.isSysTenantId()
|
||||
? dao.findByResource(ref, MAX_ENTITIES_TO_FIND)
|
||||
: dao.findByTenantIdAndResource(tenantId, ref, MAX_ENTITIES_TO_FIND);
|
||||
if (!entities.isEmpty()) {
|
||||
references.put(entityType.name(), entities);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
|
||||
deleteResource(tenantId, (TbResourceId) id, force);
|
||||
@ -663,6 +691,12 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
|
||||
return saveResource(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TbResourceInfo> findSystemOrTenantResourcesByIds(TenantId tenantId, List<TbResourceId> resourceIds) {
|
||||
log.trace("Executing findSystemOrTenantResourcesByIds, tenantId [{}], resourceIds [{}]", tenantId, resourceIds);
|
||||
return resourceInfoDao.findSystemOrTenantResourcesByIds(tenantId, resourceIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String calculateEtag(byte[] data) {
|
||||
return Hashing.sha256().hashBytes(data).toString();
|
||||
|
||||
@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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.resource;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.google.common.util.concurrent.FluentFuture;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thingsboard.common.util.DonAsynchron;
|
||||
import org.thingsboard.server.common.data.TbResourceDataInfo;
|
||||
import org.thingsboard.server.common.data.id.TbResourceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.dao.sql.JpaExecutorService;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class DefaultTbResourceDataCache implements TbResourceDataCache {
|
||||
|
||||
private final ResourceService resourceService;
|
||||
private final JpaExecutorService executorService;
|
||||
|
||||
@Value("${cache.tbResourceData.maxSize:100000}")
|
||||
private int cacheMaxSize;
|
||||
@Value("${cache.tbResourceData.timeToLiveInMinutes:44640}")
|
||||
private int cacheValueTtl;
|
||||
private AsyncLoadingCache<ResourceDataKey, TbResourceDataInfo> cache;
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
cache = Caffeine.newBuilder()
|
||||
.maximumSize(cacheMaxSize)
|
||||
.expireAfterAccess(cacheValueTtl, TimeUnit.MINUTES)
|
||||
.executor(executorService)
|
||||
.buildAsync((key, executor) -> CompletableFuture.supplyAsync(() -> resourceService.getResourceDataInfo(key.tenantId(), key.resourceId()), executor));
|
||||
}
|
||||
|
||||
@Override
|
||||
public FluentFuture<TbResourceDataInfo> getResourceDataInfoAsync(TenantId tenantId, TbResourceId resourceId) {
|
||||
log.trace("Retrieving resource data info by id [{}], tenant id [{}] from cache", resourceId, tenantId);
|
||||
return DonAsynchron.toFluentFuture(cache.get(new ResourceDataKey(tenantId, resourceId)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evictResourceData(TenantId tenantId, TbResourceId resourceId) {
|
||||
cache.asMap().remove(new ResourceDataKey(tenantId, resourceId));
|
||||
log.trace("Evicted resource data info with id [{}], tenant id [{}]", resourceId, tenantId);
|
||||
}
|
||||
|
||||
record ResourceDataKey (TenantId tenantId, TbResourceId resourceId) {}
|
||||
|
||||
}
|
||||
@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource;
|
||||
import org.thingsboard.server.common.data.ResourceSubType;
|
||||
import org.thingsboard.server.common.data.ResourceType;
|
||||
import org.thingsboard.server.common.data.TbResource;
|
||||
import org.thingsboard.server.common.data.TbResourceDataInfo;
|
||||
import org.thingsboard.server.common.data.id.TbResourceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.page.PageData;
|
||||
@ -51,4 +52,5 @@ public interface TbResourceDao extends Dao<TbResource>, TenantEntityWithDataDao,
|
||||
|
||||
long getResourceSize(TenantId tenantId, TbResourceId resourceId);
|
||||
|
||||
TbResourceDataInfo getResourceDataInfo(TenantId tenantId, TbResourceId resourceId);
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource;
|
||||
import org.thingsboard.server.common.data.ResourceType;
|
||||
import org.thingsboard.server.common.data.TbResourceInfo;
|
||||
import org.thingsboard.server.common.data.TbResourceInfoFilter;
|
||||
import org.thingsboard.server.common.data.id.TbResourceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.page.PageData;
|
||||
import org.thingsboard.server.common.data.page.PageLink;
|
||||
@ -46,4 +47,5 @@ public interface TbResourceInfoDao extends Dao<TbResourceInfo> {
|
||||
|
||||
TbResourceInfo findPublicResourceByKey(ResourceType resourceType, String publicResourceKey);
|
||||
|
||||
List<TbResourceInfo> findSystemOrTenantResourcesByIds(TenantId tenantId, List<TbResourceId> resourceIds);
|
||||
}
|
||||
|
||||
@ -21,8 +21,10 @@ import org.thingsboard.server.common.data.page.PageData;
|
||||
import org.thingsboard.server.common.data.page.PageLink;
|
||||
import org.thingsboard.server.common.data.rule.RuleChain;
|
||||
import org.thingsboard.server.common.data.rule.RuleChainType;
|
||||
import org.thingsboard.server.common.data.rule.RuleNode;
|
||||
import org.thingsboard.server.dao.Dao;
|
||||
import org.thingsboard.server.dao.ExportableEntityDao;
|
||||
import org.thingsboard.server.dao.ResourceContainerDao;
|
||||
import org.thingsboard.server.dao.TenantEntityDao;
|
||||
|
||||
import java.util.Collection;
|
||||
@ -31,7 +33,7 @@ import java.util.UUID;
|
||||
/**
|
||||
* Created by igor on 3/12/18.
|
||||
*/
|
||||
public interface RuleChainDao extends Dao<RuleChain>, TenantEntityDao<RuleChain>, ExportableEntityDao<RuleChainId, RuleChain> {
|
||||
public interface RuleChainDao extends Dao<RuleChain>, TenantEntityDao<RuleChain>, ExportableEntityDao<RuleChainId, RuleChain>, ResourceContainerDao<RuleChain> {
|
||||
|
||||
/**
|
||||
* Find rule chains by tenantId and page link.
|
||||
|
||||
@ -20,6 +20,7 @@ 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.thingsboard.server.common.data.EntityInfo;
|
||||
import org.thingsboard.server.dao.model.sql.DashboardInfoEntity;
|
||||
|
||||
import java.util.List;
|
||||
@ -87,12 +88,15 @@ public interface DashboardInfoRepository extends JpaRepository<DashboardInfoEnti
|
||||
)
|
||||
List<DashboardInfoEntity> findByImageLink(@Param("imageLink") String imageLink, @Param("limit") int limit);
|
||||
|
||||
@Query(value = "SELECT * FROM dashboard d WHERE d.tenant_id = :tenantId and d.configuration ILIKE CONCAT('%', :link, '%') limit :limit",
|
||||
nativeQuery = true)
|
||||
List<DashboardInfoEntity> findDashboardInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, @Param("link") String link, @Param("limit") int limit);
|
||||
@Query("SELECT new org.thingsboard.server.common.data.EntityInfo(d.id, 'DASHBOARD', d.title) " +
|
||||
"FROM DashboardEntity d WHERE d.tenantId = :tenantId AND ilike(cast(d.configuration as string), CONCAT('%', :link, '%')) = true")
|
||||
List<EntityInfo> findDashboardInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId,
|
||||
@Param("link") String link,
|
||||
Pageable pageable);
|
||||
|
||||
@Query(value = "SELECT * FROM dashboard d WHERE d.configuration ILIKE CONCAT('%', :link, '%') limit :limit",
|
||||
nativeQuery = true)
|
||||
List<DashboardInfoEntity> findDashboardInfosByResourceLink(@Param("link") String link, @Param("limit") int limit);
|
||||
@Query("SELECT new org.thingsboard.server.common.data.EntityInfo(d.id, 'DASHBOARD', d.title) " +
|
||||
"FROM DashboardEntity d WHERE ilike(cast(d.configuration as string), CONCAT('%', :link, '%')) = true")
|
||||
List<EntityInfo> findDashboardInfosByResourceLink(@Param("link") String link,
|
||||
Pageable pageable);
|
||||
|
||||
}
|
||||
|
||||
@ -17,9 +17,11 @@ package org.thingsboard.server.dao.sql.dashboard;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thingsboard.server.common.data.DashboardInfo;
|
||||
import org.thingsboard.server.common.data.EntityInfo;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.page.PageData;
|
||||
import org.thingsboard.server.common.data.page.PageLink;
|
||||
@ -135,13 +137,13 @@ public class JpaDashboardInfoDao extends JpaAbstractDao<DashboardInfoEntity, Das
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DashboardInfo> findByTenantIdAndResourceLink(TenantId tenantId, String url, int limit) {
|
||||
return DaoUtil.convertDataList(dashboardInfoRepository.findDashboardInfosByTenantIdAndResourceLink(tenantId.getId(), url, limit));
|
||||
public List<EntityInfo> findByTenantIdAndResource(TenantId tenantId, String reference, int limit) {
|
||||
return dashboardInfoRepository.findDashboardInfosByTenantIdAndResourceLink(tenantId.getId(), reference, PageRequest.of(0, limit));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DashboardInfo> findByResourceLink(String link, int limit) {
|
||||
return DaoUtil.convertDataList(dashboardInfoRepository.findDashboardInfosByResourceLink(link, limit));
|
||||
public List<EntityInfo> findByResource(String reference, int limit) {
|
||||
return dashboardInfoRepository.findDashboardInfosByResourceLink(reference, PageRequest.of(0, limit));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
|
||||
import org.thingsboard.server.common.data.ResourceSubType;
|
||||
import org.thingsboard.server.common.data.ResourceType;
|
||||
import org.thingsboard.server.common.data.TbResource;
|
||||
import org.thingsboard.server.common.data.TbResourceDataInfo;
|
||||
import org.thingsboard.server.common.data.id.TbResourceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.page.PageData;
|
||||
@ -115,6 +116,11 @@ public class JpaTbResourceDao extends JpaAbstractDao<TbResourceEntity, TbResourc
|
||||
return resourceRepository.getDataSizeById(resourceId.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TbResourceDataInfo getResourceDataInfo(TenantId tenantId, TbResourceId resourceId) {
|
||||
return resourceRepository.getDataInfoById(resourceId.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long sumDataSizeByTenantId(TenantId tenantId) {
|
||||
return resourceRepository.sumDataSizeByTenantId(tenantId.getId());
|
||||
|
||||
@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.ResourceSubType;
|
||||
import org.thingsboard.server.common.data.ResourceType;
|
||||
import org.thingsboard.server.common.data.TbResourceInfo;
|
||||
import org.thingsboard.server.common.data.TbResourceInfoFilter;
|
||||
import org.thingsboard.server.common.data.id.TbResourceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.page.PageData;
|
||||
import org.thingsboard.server.common.data.page.PageLink;
|
||||
@ -40,6 +41,8 @@ import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@SqlDao
|
||||
@ -126,4 +129,9 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao<TbResourceInfoEntity, T
|
||||
public TbResourceInfo findPublicResourceByKey(ResourceType resourceType, String publicResourceKey) {
|
||||
return DaoUtil.getData(resourceInfoRepository.findByResourceTypeAndPublicResourceKeyAndIsPublicTrue(resourceType.name(), publicResourceKey));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TbResourceInfo> findSystemOrTenantResourcesByIds(TenantId tenantId, List<TbResourceId> resourceIds) {
|
||||
return DaoUtil.convertDataList(resourceInfoRepository.findSystemOrTenantResourcesByIdIn(tenantId.getId(), TenantId.NULL_UUID, toUUIDs(resourceIds)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,4 +79,10 @@ public interface TbResourceInfoRepository extends JpaRepository<TbResourceInfoEn
|
||||
|
||||
TbResourceInfoEntity findByResourceTypeAndPublicResourceKeyAndIsPublicTrue(String resourceType, String publicResourceKey);
|
||||
|
||||
@Query("SELECT tr FROM TbResourceInfoEntity tr WHERE " +
|
||||
"tr.id IN (:resourceIds) AND (tr.tenantId = :tenantId OR tr.tenantId = :systemTenantId)")
|
||||
List<TbResourceInfoEntity> findSystemOrTenantResourcesByIdIn(@Param("tenantId") UUID tenantId,
|
||||
@Param("systemTenantId") UUID systemTenantId,
|
||||
@Param("resourceIds") List<UUID> resourceIds);
|
||||
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ 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.thingsboard.server.common.data.TbResourceDataInfo;
|
||||
import org.thingsboard.server.dao.ExportableEntityRepository;
|
||||
import org.thingsboard.server.dao.model.sql.TbResourceEntity;
|
||||
|
||||
@ -101,4 +102,6 @@ public interface TbResourceRepository extends JpaRepository<TbResourceEntity, UU
|
||||
@Query("SELECT r.id FROM TbResourceInfoEntity r WHERE r.tenantId = :tenantId")
|
||||
Page<UUID> findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable);
|
||||
|
||||
@Query("SELECT new org.thingsboard.server.common.data.TbResourceDataInfo(r.data, r.descriptor) FROM TbResourceEntity r WHERE r.id = :id")
|
||||
TbResourceDataInfo getDataInfoById(UUID id);
|
||||
}
|
||||
|
||||
@ -18,8 +18,10 @@ package org.thingsboard.server.dao.sql.rule;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Limit;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thingsboard.server.common.data.EntityInfo;
|
||||
import org.thingsboard.server.common.data.EntityType;
|
||||
import org.thingsboard.server.common.data.edqs.fields.RuleChainFields;
|
||||
import org.thingsboard.server.common.data.id.RuleChainId;
|
||||
@ -141,6 +143,16 @@ public class JpaRuleChainDao extends JpaAbstractDao<RuleChainEntity, RuleChain>
|
||||
return findRuleChainsByTenantId(tenantId.getId(), pageLink);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EntityInfo> findByTenantIdAndResource(TenantId tenantId, String reference, int limit) {
|
||||
return ruleChainRepository.findRuleChainsByTenantIdAndResource(tenantId.getId(), reference, PageRequest.of(0, limit));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EntityInfo> findByResource(String reference, int limit) {
|
||||
return ruleChainRepository.findRuleChainsByResource(reference, PageRequest.of(0, limit));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RuleChainFields> findNextBatch(UUID id, int batchSize) {
|
||||
return ruleChainRepository.findNextBatch(id, Limit.of(batchSize));
|
||||
|
||||
@ -17,10 +17,12 @@ package org.thingsboard.server.dao.sql.rule;
|
||||
|
||||
import org.springframework.data.domain.Limit;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
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.thingsboard.server.common.data.EntityInfo;
|
||||
import org.thingsboard.server.common.data.edqs.fields.RuleChainFields;
|
||||
import org.thingsboard.server.common.data.rule.RuleChainType;
|
||||
import org.thingsboard.server.dao.ExportableEntityRepository;
|
||||
@ -72,6 +74,19 @@ public interface RuleChainRepository extends JpaRepository<RuleChainEntity, UUID
|
||||
@Query("SELECT externalId FROM RuleChainEntity WHERE id = :id")
|
||||
UUID getExternalIdById(@Param("id") UUID id);
|
||||
|
||||
@Query("SELECT new org.thingsboard.server.common.data.EntityInfo(rc.id, 'RULE_CHAIN', rc.name) " +
|
||||
"FROM RuleChainEntity rc WHERE rc.tenantId = :tenantId AND EXISTS " +
|
||||
"(SELECT 1 FROM RuleNodeEntity rn WHERE rn.ruleChainId = rc.id AND cast(rn.configuration as string) LIKE CONCAT('%', :resourceId, '%'))")
|
||||
List<EntityInfo> findRuleChainsByTenantIdAndResource(@Param("tenantId") UUID tenantId,
|
||||
@Param("resourceId") String resourceId,
|
||||
PageRequest of);
|
||||
|
||||
@Query("SELECT new org.thingsboard.server.common.data.EntityInfo(rc.id, 'RULE_CHAIN', rc.name) " +
|
||||
"FROM RuleChainEntity rc WHERE EXISTS " +
|
||||
"(SELECT 1 FROM RuleNodeEntity rn WHERE rn.ruleChainId = rc.id AND cast(rn.configuration as string) LIKE CONCAT('%', :resourceId, '%'))")
|
||||
List<EntityInfo> findRuleChainsByResource(@Param("resourceId") String resourceId,
|
||||
Pageable pageable);
|
||||
|
||||
@Query("SELECT new org.thingsboard.server.common.data.edqs.fields.RuleChainFields(r.id, r.createdTime, r.tenantId," +
|
||||
"r.name, r.version, r.additionalInfo) FROM RuleChainEntity r WHERE r.id > :id ORDER BY r.id")
|
||||
List<RuleChainFields> findNextBatch(@Param("id") UUID id, Limit limit);
|
||||
|
||||
@ -17,8 +17,10 @@ package org.thingsboard.server.dao.sql.widget;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Limit;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thingsboard.server.common.data.EntityInfo;
|
||||
import org.thingsboard.server.common.data.EntityType;
|
||||
import org.thingsboard.server.common.data.edqs.fields.WidgetTypeFields;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
@ -269,13 +271,13 @@ public class JpaWidgetTypeDao extends JpaAbstractDao<WidgetTypeDetailsEntity, Wi
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WidgetTypeInfo> findByTenantIdAndResourceLink(TenantId tenantId, String link, int limit) {
|
||||
return DaoUtil.convertDataList(widgetTypeInfoRepository.findWidgetTypeInfosByTenantIdAndResourceLink(tenantId.getId(), link, limit));
|
||||
public List<EntityInfo> findByTenantIdAndResource(TenantId tenantId, String reference, int limit) {
|
||||
return widgetTypeInfoRepository.findWidgetTypeInfosByTenantIdAndResourceLink(tenantId.getId(), reference, PageRequest.of(0, limit));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WidgetTypeInfo> findByResourceLink(String link, int limit) {
|
||||
return DaoUtil.convertDataList(widgetTypeInfoRepository.findWidgetTypeInfosByResourceLink(link, limit));
|
||||
public List<EntityInfo> findByResource(String reference, int limit) {
|
||||
return widgetTypeInfoRepository.findWidgetTypeInfosByResourceLink(reference, PageRequest.of(0, limit));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -20,6 +20,7 @@ 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.thingsboard.server.common.data.EntityInfo;
|
||||
import org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity;
|
||||
|
||||
import java.util.List;
|
||||
@ -214,10 +215,14 @@ public interface WidgetTypeInfoRepository extends JpaRepository<WidgetTypeInfoEn
|
||||
)
|
||||
List<WidgetTypeInfoEntity> findByImageUrl(@Param("imageLink") String imageLink, @Param("limit") int limit);
|
||||
|
||||
@Query(value = "SELECT * FROM widget_type_info_view w WHERE w.tenant_id = :tenantId AND w.descriptor ILIKE CONCAT('%', :link, '%') LIMIT :limit ", nativeQuery = true)
|
||||
List<WidgetTypeInfoEntity> findWidgetTypeInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, @Param("link") String link, @Param("limit") int limit);
|
||||
|
||||
@Query(value = "SELECT * FROM widget_type_info_view w WHERE w.descriptor ILIKE CONCAT('%', :link, '%') LIMIT :limit ", nativeQuery = true)
|
||||
List<WidgetTypeInfoEntity> findWidgetTypeInfosByResourceLink(@Param("link") String link, @Param("limit") int limit);
|
||||
@Query("SELECT new org.thingsboard.server.common.data.EntityInfo(w.id, 'WIDGET_TYPE', w.name) " +
|
||||
"FROM WidgetTypeEntity w WHERE w.tenantId = :tenantId AND ilike(cast(w.descriptor as string), CONCAT('%', :link, '%')) = true")
|
||||
List<EntityInfo> findWidgetTypeInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId,
|
||||
@Param("link") String link,
|
||||
Pageable pageable);
|
||||
|
||||
@Query("SELECT new org.thingsboard.server.common.data.EntityInfo(w.id, 'WIDGET_TYPE', w.name) " +
|
||||
"FROM WidgetTypeEntity w WHERE ilike(cast(w.descriptor as string), CONCAT('%', :link, '%')) = true")
|
||||
List<EntityInfo> findWidgetTypeInfosByResourceLink(@Param("link") String link,
|
||||
Pageable pageable);
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.server.common.data.id.RuleChainId;
|
||||
import org.thingsboard.server.common.data.id.RuleNodeId;
|
||||
import org.thingsboard.server.common.data.id.TbResourceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.page.PageData;
|
||||
import org.thingsboard.server.common.data.page.PageLink;
|
||||
|
||||
@ -24,6 +24,7 @@ import org.thingsboard.server.cluster.TbClusterService;
|
||||
import org.thingsboard.server.common.data.Customer;
|
||||
import org.thingsboard.server.common.data.Device;
|
||||
import org.thingsboard.server.common.data.DeviceProfile;
|
||||
import org.thingsboard.server.common.data.HasTenantId;
|
||||
import org.thingsboard.server.common.data.TenantProfile;
|
||||
import org.thingsboard.server.common.data.alarm.Alarm;
|
||||
import org.thingsboard.server.common.data.asset.Asset;
|
||||
@ -32,6 +33,7 @@ import org.thingsboard.server.common.data.id.AssetId;
|
||||
import org.thingsboard.server.common.data.id.CustomerId;
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
import org.thingsboard.server.common.data.id.EntityId;
|
||||
import org.thingsboard.server.common.data.id.HasId;
|
||||
import org.thingsboard.server.common.data.id.RuleChainId;
|
||||
import org.thingsboard.server.common.data.id.RuleNodeId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
@ -78,6 +80,7 @@ import org.thingsboard.server.dao.queue.QueueService;
|
||||
import org.thingsboard.server.dao.queue.QueueStatsService;
|
||||
import org.thingsboard.server.dao.relation.RelationService;
|
||||
import org.thingsboard.server.dao.resource.ResourceService;
|
||||
import org.thingsboard.server.dao.resource.TbResourceDataCache;
|
||||
import org.thingsboard.server.dao.rule.RuleChainService;
|
||||
import org.thingsboard.server.dao.tenant.TenantService;
|
||||
import org.thingsboard.server.dao.timeseries.TimeseriesService;
|
||||
@ -252,6 +255,8 @@ public interface TbContext {
|
||||
|
||||
void checkTenantEntity(EntityId entityId) throws TbNodeException;
|
||||
|
||||
<E extends HasId<I> & HasTenantId, I extends EntityId> void checkTenantEntity(E entity) throws TbNodeException;
|
||||
|
||||
boolean isLocalEntity(EntityId entityId);
|
||||
|
||||
RuleNodeId getSelfId();
|
||||
@ -308,6 +313,8 @@ public interface TbContext {
|
||||
|
||||
ResourceService getResourceService();
|
||||
|
||||
TbResourceDataCache getTbResourceDataCache();
|
||||
|
||||
OtaPackageService getOtaPackageService();
|
||||
|
||||
RuleEngineDeviceProfileCache getDeviceProfileCache();
|
||||
|
||||
@ -18,12 +18,20 @@ 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 com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.data.message.Content;
|
||||
import dev.langchain4j.data.message.ImageContent;
|
||||
import dev.langchain4j.data.message.PdfFileContent;
|
||||
import dev.langchain4j.data.message.SystemMessage;
|
||||
import dev.langchain4j.data.message.TextContent;
|
||||
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.response.ChatResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.rule.engine.api.RuleNode;
|
||||
@ -33,24 +41,38 @@ 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.GeneralFileDescriptor;
|
||||
import org.thingsboard.server.common.data.ResourceType;
|
||||
import org.thingsboard.server.common.data.TbResourceDataInfo;
|
||||
import org.thingsboard.server.common.data.TbResourceInfo;
|
||||
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.AiChatModelConfig;
|
||||
import org.thingsboard.server.common.data.id.AiModelId;
|
||||
import org.thingsboard.server.common.data.id.TbResourceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
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;
|
||||
import org.thingsboard.server.dao.resource.TbResourceDataCache;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
|
||||
import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbResponseFormatType;
|
||||
import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields;
|
||||
|
||||
@Slf4j
|
||||
@RuleNode(
|
||||
type = ComponentType.EXTERNAL,
|
||||
name = "AI request",
|
||||
@ -77,6 +99,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
|
||||
|
||||
private String systemPrompt;
|
||||
private String userPrompt;
|
||||
private Set<TbResourceId> resourceIds;
|
||||
private ResponseFormat responseFormat;
|
||||
private int timeoutSeconds;
|
||||
private AiModelId modelId;
|
||||
@ -111,6 +134,14 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
|
||||
// LangChain4j AnthropicChatModel rejects requests with non-null ResponseFormat even if ResponseFormatType is TEXT
|
||||
responseFormat = config.getResponseFormat().toLangChainResponseFormat();
|
||||
}
|
||||
if (config.getResourceIds() != null && !config.getResourceIds().isEmpty()) {
|
||||
resourceIds = new HashSet<>(config.getResourceIds().size());
|
||||
for (UUID resourceId : config.getResourceIds()) {
|
||||
TbResourceId tbResourceId = new TbResourceId(resourceId);
|
||||
validateResource(ctx, tbResourceId);
|
||||
resourceIds.add(tbResourceId);
|
||||
}
|
||||
}
|
||||
|
||||
systemPrompt = config.getSystemPrompt();
|
||||
userPrompt = config.getUserPrompt();
|
||||
@ -126,12 +157,42 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
|
||||
@Override
|
||||
public void onMsg(TbContext ctx, TbMsg msg) {
|
||||
var ackedMsg = ackIfNeeded(ctx, msg);
|
||||
final String processedUserPrompt = TbNodeUtils.processPattern(this.userPrompt, ackedMsg);
|
||||
|
||||
final ListenableFuture<UserMessage> userMessageFuture =
|
||||
resourceIds == null
|
||||
? Futures.immediateFuture(UserMessage.from(processedUserPrompt))
|
||||
: Futures.transform(
|
||||
loadResources(ctx),
|
||||
resources -> UserMessage.from(buildContents(processedUserPrompt, resources)),
|
||||
ctx.getDbCallbackExecutor()
|
||||
);
|
||||
|
||||
Futures.addCallback(
|
||||
userMessageFuture,
|
||||
new FutureCallback<>() {
|
||||
@Override
|
||||
public void onSuccess(UserMessage userMessage) {
|
||||
buildAndSendRequest(ctx, ackedMsg, userMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
tellFailure(ctx, ackedMsg, t);
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor()
|
||||
);
|
||||
}
|
||||
|
||||
private void buildAndSendRequest(TbContext ctx, TbMsg ackedMsg, UserMessage userMessage) {
|
||||
List<ChatMessage> chatMessages = new ArrayList<>(2);
|
||||
if (systemPrompt != null) {
|
||||
|
||||
if (systemPrompt != null && !systemPrompt.isBlank()) {
|
||||
chatMessages.add(SystemMessage.from(TbNodeUtils.processPattern(systemPrompt, ackedMsg)));
|
||||
}
|
||||
chatMessages.add(UserMessage.from(TbNodeUtils.processPattern(userPrompt, ackedMsg)));
|
||||
|
||||
chatMessages.add(userMessage);
|
||||
|
||||
var chatRequest = ChatRequest.builder()
|
||||
.messages(chatMessages)
|
||||
@ -192,11 +253,67 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
|
||||
return JacksonUtil.newObjectNode().put("response", response).toString();
|
||||
}
|
||||
|
||||
private void validateResource(TbContext ctx, TbResourceId tbResourceId) throws TbNodeException {
|
||||
TbResourceInfo resource = ctx.getResourceService().findResourceInfoById(ctx.getTenantId(), tbResourceId);
|
||||
if (resource == null) {
|
||||
throw new TbNodeException("[" + ctx.getTenantId() + "] Resource with ID: [" + tbResourceId + "] was not found", true);
|
||||
}
|
||||
if (!ResourceType.GENERAL.equals(resource.getResourceType())) {
|
||||
throw new TbNodeException("[" + ctx.getTenantId() + "] Resource with ID: [" + tbResourceId + "] has unsupported resource type: " + resource.getResourceType(), true);
|
||||
}
|
||||
ctx.checkTenantEntity(resource);
|
||||
}
|
||||
|
||||
private ListenableFuture<List<TbResourceDataInfo>> loadResources(TbContext ctx) {
|
||||
final TenantId tenantId = ctx.getTenantId();
|
||||
final TbResourceDataCache cache = ctx.getTbResourceDataCache();
|
||||
List<? extends ListenableFuture<TbResourceDataInfo>> futures = resourceIds.stream()
|
||||
.map(id -> cache.getResourceDataInfoAsync(tenantId, id))
|
||||
.toList();
|
||||
return Futures.allAsList(futures);
|
||||
}
|
||||
|
||||
private List<Content> buildContents(String userPrompt, List<TbResourceDataInfo> resources) {
|
||||
List<Content> contents = new ArrayList<>(1 + resources.size());
|
||||
contents.add(new TextContent(userPrompt)); // user prompt first
|
||||
|
||||
resources.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(this::toContent)
|
||||
.forEach(contents::add);
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
private Content toContent(TbResourceDataInfo resource) {
|
||||
if (resource.getDescriptor() == null) {
|
||||
throw new RuntimeException("Missing descriptor for resource");
|
||||
}
|
||||
GeneralFileDescriptor descriptor = JacksonUtil.treeToValue(resource.getDescriptor(), GeneralFileDescriptor.class);
|
||||
String mediaType = descriptor.getMediaType();
|
||||
if (mediaType == null) {
|
||||
throw new RuntimeException("Missing mediaType in resource descriptor " + resource.getDescriptor());
|
||||
}
|
||||
byte[] data = resource.getData();
|
||||
if (mediaType.startsWith("text/")) {
|
||||
return new TextContent(new String(data, StandardCharsets.UTF_8));
|
||||
}
|
||||
if (mediaType.equals("application/pdf")) {
|
||||
return new PdfFileContent(Base64.getEncoder().encodeToString(data), mediaType);
|
||||
}
|
||||
if (mediaType.startsWith("image/")) {
|
||||
return new ImageContent(Base64.getEncoder().encodeToString(data), mediaType);
|
||||
}
|
||||
log.debug("Trying to create text content for {}", resource.getDescriptor());
|
||||
return new TextContent(new String(data, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
systemPrompt = null;
|
||||
userPrompt = null;
|
||||
resourceIds = null;
|
||||
responseFormat = null;
|
||||
modelId = null;
|
||||
}
|
||||
|
||||
@ -20,12 +20,14 @@ 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.rule.engine.api.NodeConfiguration;
|
||||
import org.thingsboard.server.common.data.id.AiModelId;
|
||||
import org.thingsboard.server.common.data.validation.Length;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseFormat;
|
||||
|
||||
@Data
|
||||
@ -41,6 +43,8 @@ public class TbAiNodeConfiguration implements NodeConfiguration<TbAiNodeConfigur
|
||||
@Length(min = 1, max = 500_000)
|
||||
private String userPrompt;
|
||||
|
||||
private Set<@NotNull(message = "references to resources cannot be null") UUID> resourceIds;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private TbResponseFormat responseFormat;
|
||||
|
||||
@ -17,8 +17,11 @@ package org.thingsboard.rule.engine.ai;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.google.common.util.concurrent.FluentFuture;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import dev.langchain4j.data.message.AiMessage;
|
||||
import dev.langchain4j.data.message.ImageContent;
|
||||
import dev.langchain4j.data.message.SystemMessage;
|
||||
import dev.langchain4j.data.message.TextContent;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import dev.langchain4j.model.chat.request.ResponseFormat;
|
||||
import dev.langchain4j.model.chat.request.ResponseFormatType;
|
||||
@ -32,6 +35,7 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
@ -43,6 +47,10 @@ import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
|
||||
import org.thingsboard.rule.engine.api.TbContext;
|
||||
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
|
||||
import org.thingsboard.rule.engine.api.TbNodeException;
|
||||
import org.thingsboard.server.common.data.GeneralFileDescriptor;
|
||||
import org.thingsboard.server.common.data.ResourceType;
|
||||
import org.thingsboard.server.common.data.TbResource;
|
||||
import org.thingsboard.server.common.data.TbResourceDataInfo;
|
||||
import org.thingsboard.server.common.data.ai.AiModel;
|
||||
import org.thingsboard.server.common.data.ai.model.AiModelConfig;
|
||||
import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig;
|
||||
@ -52,6 +60,7 @@ import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
|
||||
import org.thingsboard.server.common.data.id.AiModelId;
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
import org.thingsboard.server.common.data.id.RuleNodeId;
|
||||
import org.thingsboard.server.common.data.id.TbResourceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
|
||||
import org.thingsboard.server.common.data.rule.RuleNode;
|
||||
@ -59,9 +68,14 @@ import org.thingsboard.server.common.msg.TbMsg;
|
||||
import org.thingsboard.server.common.msg.TbMsgMetaData;
|
||||
import org.thingsboard.server.dao.ai.AiModelService;
|
||||
import org.thingsboard.server.dao.exception.DataValidationException;
|
||||
import org.thingsboard.server.dao.resource.ResourceService;
|
||||
import org.thingsboard.server.dao.resource.TbResourceDataCache;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@ -76,16 +90,23 @@ import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.thingsboard.server.common.data.ResourceType.GENERAL;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TbAiNodeTest {
|
||||
|
||||
private static final byte[] PNG_IMAGE = Base64.getDecoder().decode("iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAC9FBMVEUAAAABAQEBAgICAgICAwMCBAQDAwMDBQUDBgYEBAQEBwcECAgFCQkFCgoGBgYGCwsGDAwHBwcHDQ0HDg4ICAgIDw8IEBAJCQkJEREKEhIKExMLFBQLFRUMFhYMFxcNDQ0NGBgNGRkODg4OGhoOGxsPDw8PHBwPHR0QEBAQHh4QHx8RERERICARISESEhISIiITExMTIyMTJCQUJSUUJiYVKCgWFhYWKSkXFxcXGhwYGBgYLC0ZGRkaMDEaMTIbGxsbMjMcMzQdNTYfOTogICAgOzwiP0AiQEEjIyMjQkMkQ0QnJycnSEkoS0wpKSkrUFErUVIsLCwvV1gvWFkwWlszMzMzYGE1NTU2NjY3Zmc4aWo5OTk5ams5a2w6Ojo6bG07bm88cXI9cnM9c3Q/dndAQEBAeHlBeXpCQkJCe3xCfH1DQ0NEREREf4FFRUVFgIJGg4VHhYdISEhIhohJh4lLi41LjI5MTExMjpBNj5FNkJJOkpRQUFBQlZdRUVFSUlJTU1NTmpxUVFRUnZ9VVVVVnqBWVlZYpadZWVlZp6laqKpbW1tbqatbqqxcXFxcrK5dra9drrBeXl5er7FfsbNfsrRgs7VhYWFiYmJiuLpjubtku71lvL5lvb9mvsBnwcNowsRpxMZpxcdra2tryctsysxubm5uzc9vb29vz9Fw0dNx0tVy1Ndy1dhz1tlz19p0dHR02Nt02dx12t1229523N93d3d33eB33uF5eXl54eR6enp64+Z65Od75eh75ul8fHx85+p86Ot96ex96u2AgICA7vGA7/KB8fSC8/aD9PeD9fiEhISE9/qF+PuF+fyGhoaG+v2G+/6Hh4eH/P+IiIiMjIyNjY2Ojo6QkJCRkZGSkpKTk5Obm5ucnJyfn5+lpaWnp6eoqKipqamqqqqwsLCzs7O1tbW4uLi5ubm6urq7u7u8vLy/v7/BwcHCwsLFxcXGxsbPz8/Y2Nji4uLj4+Pv7+/4+Pj5+fn+/v7/75T///+GLm1tAAAAAWJLR0T7omo23AAABJtJREFUeNrt3Wd8E3UYB/CH0oqm1dJaS5N0IKu0qQSVinXG4gKlKFi3uMC9FVwoVQnQqCBgBVxFnKCoFFFExFGhliWt/zoYLuIMKEpB7b3xuf9dQu+MvAjXcsTf7/PJk/ul1/S+TS53r3KkNFfk0V6evDHbFGruQ3EQTzNVUFxkHOXFB6QbIQiCIAiC/GeSs/QkR6vkCPeUaNUeSUjkkdR1npCp6a7VV7U6P1dbKfNFrS89rJNas/T6rlZtkUS/i2evhw99Q92y9/r7nVzzw7VfeDX3y2qv893plTVb1uW+uw6xiyNpspAQ8bjLy8l5REiImOlUq3Pniunyxw8Ib+vqF7aB5AgdItLVmit0iOgc9W0owhDt1RSAABL3EGeDDqmXhwRXgw6pj3qESFhtgHC1DYSGrJCQjweFq4SEqzkD67zGah8Inay+p1yl4XqKWt2lF69UDxQrzzevXZprrDn2gfTIUs85Iv/oHpny8HKHdugeVZhpXNudu6u6J1P8lmpIX1ys10X6myVfPeLl919UZFi74JXjWtfCecfa5sj+odx908XSg9Taqdaw+3I1QuYLA6RG2AbiEDpE9JJnvcYP1BRhgiw3QuoAASTuIQnP6JCF8hQlcbYBwrWIKgPDIg9UGSGP2QdCnZ+QkDneKQs4swqe1CDJ09RaXfBUETWKm3a+gFMMEMc0+0AoJVX9nM1+VDsCznLurz64b5VWq7nWLLi81QfygYZfNlU7nAUP0nOwrLnGiiAIgiAIgiAIgiDI/zstLS3tMEtKSiycgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBYAkEQBEEQBEEQBEEQBGmrdLwuyLmhg703km8Z63k7N2Tw0jnqFt/f0bROn69WBYOfbuxiyR+8MXC9vB8QCBTQkEAgMOG2gVyvDmTzdAWuifFp077m8f503vwZr/PSd28Hg+uaTjVDlOFEIxVrINVijfwi4glCHE1XioXPz6kX9xHNFIUkvyM/xqeduIPHup95bGni8edYotOUqJCrrII0iMv4LnNFg4Sczd/9/Zw4abchD0Ygv0pIBVFZG0Nq587lu/PE02EIXSQuaSfI92l88bfNFkHqLxUnEM1+bXQEMloMY8hgn893esyQIzbzWHtveXn51GW89AtfTeyATWZIWm919s6wBtLYdfXdVCyuuEdCHhoxwr/mAzdDtMQKoaP4duQmRVG+kUtyu83X3OuylX09f+9r0c6eOvkjx82fdPdLiHrdjsrD1Z39LP5W06ExQ475g8eqSR6PZ+oXvLSVNWk/nmmGKNcSXaBYBXEPFkMXV1GlhFyYlSof3t19ZOxfPJp+4/HTeh47JhGdqLQxJDtpyRJxBgUi+0g7QkYSlVsHoVtFrcNiyO0SsoXHDxIykej4v/8F+XxDKLRxmXWQfo2jyGJIh894PDs9FArNeIGXvlwbCn37Upl5rXObOMPtf1K4z5u8ne/sx0tl6hbfgtNkBEGQPZs4uUBwTxoTH5DxtM0TD46+20lpHrfXX7e52/jtyj9kFKbIT2L3FQAAAABJRU5ErkJggg==");
|
||||
|
||||
@Mock
|
||||
TbContext ctxMock;
|
||||
@Mock
|
||||
AiModelService aiModelServiceMock;
|
||||
@Mock
|
||||
RuleEngineAiChatModelService aiChatModelServiceMock;
|
||||
@Mock
|
||||
TbResourceDataCache tbResourceDataCacheMock;
|
||||
@Mock
|
||||
ResourceService resourceServiceMock;
|
||||
|
||||
TbAiNode aiNode;
|
||||
TbAiNodeConfiguration config;
|
||||
@ -141,6 +162,8 @@ class TbAiNodeTest {
|
||||
lenient().when(ctxMock.getAiModelService()).thenReturn(aiModelServiceMock);
|
||||
lenient().when(ctxMock.getAiChatModelService()).thenReturn(aiChatModelServiceMock);
|
||||
lenient().when(ctxMock.getDbCallbackExecutor()).thenReturn(new TestDbCallbackExecutor());
|
||||
lenient().when(ctxMock.getTbResourceDataCache()).thenReturn(tbResourceDataCacheMock);
|
||||
lenient().when(ctxMock.getResourceService()).thenReturn(resourceServiceMock);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -158,6 +181,7 @@ class TbAiNodeTest {
|
||||
assertThat(config.getResponseFormat()).isEqualTo(new TbJsonResponseFormat());
|
||||
assertThat(config.getTimeoutSeconds()).isEqualTo(60);
|
||||
assertThat(config.isForceAck()).isTrue();
|
||||
assertThat(config.getResourceIds()).isNull();
|
||||
}
|
||||
|
||||
/* -- Node initialization tests -- */
|
||||
@ -373,6 +397,36 @@ class TbAiNodeTest {
|
||||
.matches(e -> ((TbNodeException) e).isUnrecoverable());
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenNotExistingResources_whenInit_thenThrowsException() {
|
||||
// GIVEN
|
||||
config = constructValidConfig();
|
||||
UUID resourceId = UUID.randomUUID();
|
||||
config.setResourceIds(Set.of(resourceId));
|
||||
|
||||
// WHEN-THEN
|
||||
assertThatThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))))
|
||||
.isInstanceOf(TbNodeException.class)
|
||||
.hasMessageContaining("[" + tenantId + "] Resource with ID: [" + resourceId + "] was not found");
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenResourceOfWrongType_whenInit_thenThrowsException() {
|
||||
// GIVEN
|
||||
config = constructValidConfig();
|
||||
UUID resourceId = UUID.randomUUID();
|
||||
config.setResourceIds(Set.of(resourceId));
|
||||
|
||||
// WHEN-THEN
|
||||
TbResource tbResource = new TbResource();
|
||||
tbResource.setResourceType(ResourceType.DASHBOARD);
|
||||
given(resourceServiceMock.findResourceInfoById(any(), any())).willReturn(tbResource);
|
||||
|
||||
assertThatThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))))
|
||||
.isInstanceOf(TbNodeException.class)
|
||||
.hasMessageContaining("[" + tenantId + "] Resource with ID: [" + resourceId + "] has unsupported resource type: " + ResourceType.DASHBOARD);
|
||||
}
|
||||
|
||||
/* -- Message processing tests -- */
|
||||
|
||||
@Test
|
||||
@ -560,6 +614,166 @@ class TbAiNodeTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenSystemPromptAndUserPromptAndResourcesConfigured_whenOnMsg_thenRequestContainsSystemAndUserAndResourceContent() throws TbNodeException {
|
||||
String systemPrompt = "Respond with valid JSON";
|
||||
String userPrompt = "Tell me a joke";
|
||||
String textData = "Text resource content for AI request.";
|
||||
String xmlData = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><test></test>";
|
||||
|
||||
// GIVEN
|
||||
config = constructValidConfig();
|
||||
config.setSystemPrompt(systemPrompt);
|
||||
config.setUserPrompt(userPrompt);
|
||||
UUID resourceId = UUID.randomUUID();
|
||||
UUID resourceId2 = UUID.randomUUID();
|
||||
UUID resourceId3 = UUID.randomUUID();
|
||||
|
||||
config.setResourceIds(Set.of(resourceId, resourceId2, resourceId3));
|
||||
|
||||
// WHEN-THEN
|
||||
TbResource textResource = buildGeneralResource(textData.getBytes(), "text/plain");
|
||||
TbResource xmlResource = buildGeneralResource(xmlData.getBytes(), "application/xml");
|
||||
TbResource imageResource = buildGeneralResource(PNG_IMAGE, "image/png");
|
||||
|
||||
given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId)))).willReturn(textResource);
|
||||
given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId2)))).willReturn(xmlResource);
|
||||
given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId3)))).willReturn(imageResource);
|
||||
|
||||
given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId)))).willReturn(FluentFuture.from(Futures.immediateFuture(textResource.toResourceDataInfo())));
|
||||
given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId2)))).willReturn(FluentFuture.from(Futures.immediateFuture(xmlResource.toResourceDataInfo())));
|
||||
given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId3)))).willReturn(FluentFuture.from(Futures.immediateFuture(imageResource.toResourceDataInfo())));
|
||||
|
||||
aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
|
||||
|
||||
var msg = TbMsg.newMsg()
|
||||
.originator(deviceId)
|
||||
.data(TbMsg.EMPTY_JSON_OBJECT)
|
||||
.metaData(TbMsgMetaData.EMPTY)
|
||||
.build();
|
||||
|
||||
var chatResponse = ChatResponse.builder()
|
||||
.aiMessage(AiMessage.from("{\"type\":\"joke\",\"setup\":\"Why did the scarecrow win an award?\",\"punchline\":\"Because he was outstanding in his field.\"}"))
|
||||
.build();
|
||||
|
||||
given(aiChatModelServiceMock.sendChatRequestAsync(any(), any())).willReturn(FluentFuture.from(immediateFuture(chatResponse)));
|
||||
|
||||
// WHEN
|
||||
aiNode.onMsg(ctxMock, msg);
|
||||
|
||||
// THEN
|
||||
then(aiChatModelServiceMock).should().sendChatRequestAsync(any(),
|
||||
argThat(actualChatRequest -> {
|
||||
assertThat(actualChatRequest.messages()).hasSize(2);
|
||||
assertThat(actualChatRequest.messages().get(0)).isEqualTo(SystemMessage.from(systemPrompt));
|
||||
assertThat(((UserMessage)actualChatRequest.messages().get(1)).contents())
|
||||
.containsAll(List.of(new TextContent(userPrompt), new TextContent(textData),
|
||||
new TextContent(xmlData), new ImageContent(Base64.getEncoder().encodeToString(PNG_IMAGE), "image/png")));
|
||||
return true;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenNullResource_whenOnMsg_thenRequestContainsSystemAndUserPrompt() throws TbNodeException {
|
||||
// GIVEN
|
||||
config = constructValidConfig();
|
||||
UUID resourceId = UUID.randomUUID();
|
||||
config.setResourceIds(Set.of(resourceId));
|
||||
|
||||
// WHEN-THEN
|
||||
TbResource tbResource = buildGeneralResource("Text resource content for AI request.".getBytes(), "text/plain");
|
||||
|
||||
given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId)))).willReturn(tbResource);
|
||||
given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId)))).willReturn(FluentFuture.from(Futures.immediateFuture(null)));
|
||||
|
||||
aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
|
||||
|
||||
var msg = TbMsg.newMsg()
|
||||
.originator(deviceId)
|
||||
.data(TbMsg.EMPTY_JSON_OBJECT)
|
||||
.metaData(TbMsgMetaData.EMPTY)
|
||||
.build();
|
||||
|
||||
// WHEN
|
||||
aiNode.onMsg(ctxMock, msg);
|
||||
|
||||
// THEN
|
||||
then(aiChatModelServiceMock).should().sendChatRequestAsync(any(),
|
||||
argThat(actualChatRequest -> {
|
||||
assertThat(actualChatRequest.messages()).hasSize(2);
|
||||
assertThat(actualChatRequest.messages().get(0)).isEqualTo(SystemMessage.from(config.getSystemPrompt()));
|
||||
assertThat(((UserMessage)actualChatRequest.messages().get(1)).contents())
|
||||
.containsAll(List.of(new TextContent(config.getUserPrompt())));
|
||||
return true;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenResourceWithNoDescriptor_whenOnMsg_thenEnqueueForTellFailure() throws TbNodeException {
|
||||
// GIVEN
|
||||
config = constructValidConfig();
|
||||
UUID resourceId = UUID.randomUUID();
|
||||
config.setResourceIds(Set.of(resourceId));
|
||||
|
||||
// WHEN-THEN
|
||||
TbResource tbResource = buildGeneralResource("Text resource content for AI request.".getBytes(), "text/plain");
|
||||
TbResourceDataInfo resourceDataInfo = new TbResourceDataInfo(tbResource.getData(), null);
|
||||
|
||||
given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId)))).willReturn(tbResource);
|
||||
given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId)))).willReturn(FluentFuture.from(Futures.immediateFuture(resourceDataInfo)));
|
||||
|
||||
aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
|
||||
|
||||
var msg = TbMsg.newMsg()
|
||||
.originator(deviceId)
|
||||
.data(TbMsg.EMPTY_JSON_OBJECT)
|
||||
.metaData(TbMsgMetaData.EMPTY)
|
||||
.build();
|
||||
|
||||
// WHEN
|
||||
aiNode.onMsg(ctxMock, msg);
|
||||
|
||||
// THEN
|
||||
var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class);
|
||||
then(ctxMock).should().enqueueForTellFailure(any(), exceptionCaptor.capture());
|
||||
Throwable actualException = exceptionCaptor.getValue();
|
||||
assertThat(actualException.getMessage()).isEqualTo("Missing descriptor for resource");
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenResourceWithNoMediaType_whenOnMsg_thenEnqueueForTellFailure() throws TbNodeException {
|
||||
// GIVEN
|
||||
config = constructValidConfig();
|
||||
UUID resourceId = UUID.randomUUID();
|
||||
config.setResourceIds(Set.of(resourceId));
|
||||
|
||||
// WHEN-THEN
|
||||
TbResource tbResource = buildGeneralResource("Text resource content for AI request.".getBytes(), "text/plain");
|
||||
TbResourceDataInfo resourceDataInfo = new TbResourceDataInfo(tbResource.getData(), JacksonUtil.newObjectNode());
|
||||
|
||||
given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId)))).willReturn(tbResource);
|
||||
given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId)))).willReturn(FluentFuture.from(Futures.immediateFuture(resourceDataInfo)));
|
||||
|
||||
aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
|
||||
|
||||
var msg = TbMsg.newMsg()
|
||||
.originator(deviceId)
|
||||
.data(TbMsg.EMPTY_JSON_OBJECT)
|
||||
.metaData(TbMsgMetaData.EMPTY)
|
||||
.build();
|
||||
|
||||
// WHEN
|
||||
aiNode.onMsg(ctxMock, msg);
|
||||
|
||||
// THEN
|
||||
var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class);
|
||||
then(ctxMock).should().enqueueForTellFailure(any(), exceptionCaptor.capture());
|
||||
Throwable actualException = exceptionCaptor.getValue();
|
||||
assertThat(actualException.getMessage()).isEqualTo("Missing mediaType in resource descriptor {}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenTemplatedPrompts_whenOnMsg_thenRequestContainsSubstitutedMessages() throws TbNodeException {
|
||||
// GIVEN
|
||||
@ -950,4 +1164,13 @@ class TbAiNodeTest {
|
||||
then(ctxMock).should(never()).tellFailure(any(), any());
|
||||
}
|
||||
|
||||
private TbResource buildGeneralResource(byte[] data, String mediaType) {
|
||||
TbResource tbResource = new TbResource();
|
||||
tbResource.setResourceType(GENERAL);
|
||||
GeneralFileDescriptor descriptor = new GeneralFileDescriptor(mediaType);
|
||||
tbResource.setDescriptorValue(descriptor);
|
||||
tbResource.setData(data);
|
||||
return tbResource;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user