Browse Source

AI rule node: REST API for CRUD operations on AI settings

pull/13371/head
Dmytro Skarzhynets 1 year ago
parent
commit
1a3b1c3fe4
No known key found for this signature in database GPG Key ID: 2B51652F224037DF
  1. 123
      application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java
  2. 13
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  3. 3
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  4. 3
      application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
  5. 17
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  6. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java
  7. 6
      common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java
  8. 8
      dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java
  9. 14
      dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java
  10. 15
      dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java

123
application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java

@ -15,66 +15,135 @@
*/
package org.thingsboard.server.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.ai.AiSettings;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AiSettingsId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.ai.AiSettingsService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
// TODO: TbAiSettingsService?
import static org.thingsboard.server.controller.ControllerConstants.AI_SETTINGS_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/ai-settings")
public class AiSettingsController extends BaseController {
private final AiSettingsService aiSettingsService;
private static final Set<String> ALLOWED_SORT_PROPERTIES = Set.of("createdTime", "name", "provider", "model");
@ApiOperation(
value = "Create or update AI settings (saveAiSettings)",
notes = "Creates or updates an AI settings record.\n\n" +
"• **Create:** Omit the `id` to create a new record. The platform assigns a UUID to the new settings and returns it in the `id` field of the response.\n\n" +
"• **Update:** Include an existing `id` to modify that record. If no matching record exists, the API responds with **404 Not Found**.\n\n" +
"Tenant ID for the AI settings will be taken from the authenticated user making the request, regardless of any value provided in the request body." +
TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping
public AiSettings saveAiSettings(
@RequestBody AiSettings aiSettings,
@AuthenticationPrincipal SecurityUser requestingUser
) {
return aiSettingsService.save(requestingUser.getTenantId(), aiSettings);
public AiSettings saveAiSettings(@RequestBody AiSettings aiSettings) throws ThingsboardException {
aiSettings.setTenantId(getTenantId());
checkEntity(aiSettings.getId(), aiSettings, Resource.AI_SETTINGS);
return aiSettingsService.save(aiSettings);
}
@ApiOperation(
value = "Get AI settings by ID (getAiSettingsById)",
notes = "Fetches an AI settings record by its `id`." +
TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/{aiSettingsId}")
public AiSettings getAiSettingsById(
@PathVariable("aiSettingsId") UUID aiSettingsUuid,
@AuthenticationPrincipal SecurityUser requestingUser
@Parameter(
description = "ID of the AI settings record",
required = true,
example = "de7900d4-30e2-11f0-9cd2-0242ac120002"
)
@PathVariable("aiSettingsId") UUID aiSettingsUuid
) throws ThingsboardException {
return checkNotNull(aiSettingsService.findAiSettingsByTenantIdAndId(requestingUser.getTenantId(), new AiSettingsId(aiSettingsUuid)));
return checkAiSettingsId(new AiSettingsId(aiSettingsUuid), Operation.READ);
}
@ApiOperation(
value = "Get AI settings (getAiSettings)",
notes = "Returns a page of AI settings. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping
public PageData<AiSettings> getAllAiSettings(
@AuthenticationPrincipal SecurityUser requestingUser
) {
return aiSettingsService.findAiSettingsByTenantId(requestingUser.getTenantId(), new PageLink(Integer.MAX_VALUE));
public PageData<AiSettings> getAiSettings(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = AI_SETTINGS_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "provider", "model"}))
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder
) throws ThingsboardException {
var user = getCurrentUser();
accessControlService.checkPermission(user, Resource.AI_SETTINGS, Operation.READ);
validateSortProperty(sortProperty);
var pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return aiSettingsService.findAiSettingsByTenantId(user.getTenantId(), pageLink);
}
private static void validateSortProperty(String sortProperty) {
if (sortProperty != null && !ALLOWED_SORT_PROPERTIES.contains(sortProperty)) {
throw new IllegalArgumentException("Unsupported sort property '" + sortProperty + "'! Only '" + String.join("', '", ALLOWED_SORT_PROPERTIES) + "' are allowed.");
}
}
@ApiOperation(
value = "Delete AI settings by ID (deleteAiSettingsById)",
notes = "Deletes the AI settings record by its `id`. " +
"If a record with the specified `id` exists, the record is deleted and the endpoint returns `true`. " +
"If no such record exists, the endpoint returns `false`." +
TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@DeleteMapping("/{aiSettingsId}")
public boolean deleteAiSettingsById(
@PathVariable("aiSettingsId") UUID aiSettingsUuid,
@AuthenticationPrincipal SecurityUser requestingUser
) {
return aiSettingsService.deleteByTenantIdAndId(requestingUser.getTenantId(), new AiSettingsId(aiSettingsUuid));
@Parameter(
description = "ID of the AI settings record",
required = true,
example = "de7900d4-30e2-11f0-9cd2-0242ac120002"
)
@PathVariable("aiSettingsId") UUID aiSettingsUuid
) throws ThingsboardException {
var user = getCurrentUser();
var aiSettingsId = new AiSettingsId(aiSettingsUuid);
accessControlService.checkPermission(user, Resource.AI_SETTINGS, Operation.DELETE);
Optional<AiSettings> aiSettingsOpt = aiSettingsService.findAiSettingsByTenantIdAndId(user.getTenantId(), aiSettingsId);
if (aiSettingsOpt.isEmpty()) {
return false;
}
accessControlService.checkPermission(user, Resource.AI_SETTINGS, Operation.DELETE, aiSettingsId, aiSettingsOpt.get());
return aiSettingsService.deleteByTenantIdAndId(user.getTenantId(), aiSettingsId);
}
}

13
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -63,6 +63,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantInfo;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiSettings;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
@ -77,6 +78,7 @@ import org.thingsboard.server.common.data.edge.EdgeInfo;
import org.thingsboard.server.common.data.exception.EntityVersionMismatchException;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AiSettingsId;
import org.thingsboard.server.common.data.id.AlarmCommentId;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.AssetId;
@ -129,6 +131,7 @@ import org.thingsboard.server.common.data.util.ThrowingBiFunction;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.ai.AiSettingsService;
import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
@ -373,6 +376,9 @@ public abstract class BaseController {
@Autowired
protected CalculatedFieldService calculatedFieldService;
@Autowired
protected AiSettingsService aiSettingsService;
@Value("${server.log_controller_error_stack_trace}")
@Getter
private boolean logControllerErrorStackTrace;
@ -681,6 +687,9 @@ public abstract class BaseController {
case CALCULATED_FIELD:
checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation);
return;
case AI_SETTINGS:
checkAiSettingsId(new AiSettingsId(entityId.getId()), operation);
return;
default:
checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation);
}
@ -881,6 +890,10 @@ public abstract class BaseController {
return checkEntityId(notificationTargetId, notificationTargetService::findNotificationTargetById, operation);
}
AiSettings checkAiSettingsId(AiSettingsId aiSettingsId, Operation operation) throws ThingsboardException {
return checkEntityId(aiSettingsId, (tenantId, id) -> aiSettingsService.findAiSettingsByTenantIdAndId(tenantId, id).orElse(null), operation);
}
protected <I extends EntityId> I emptyId(EntityType entityType) {
return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);
}

3
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -31,7 +31,7 @@ public class ControllerConstants {
protected static final String ASSIGNEE_ID = "assigneeId";
protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " +
"The result is wrapped with PageData object that allows you to iterate over result set using pagination. " +
"See the 'Model' tab of the Response Class for more details. ";
"See response schema for more details. ";
protected static final String INLINE_IMAGES = "inlineImages";
protected static final String INLINE_IMAGES_DESCRIPTION = "Inline images as a data URL (Base64)";
@ -90,6 +90,7 @@ public class ControllerConstants {
protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the tenant profile name.";
protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the rule chain name.";
protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the device profile name.";
protected static final String AI_SETTINGS_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the AI settings name";
protected static final String ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset profile name.";
protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the customer title.";

3
application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java

@ -51,7 +51,8 @@ public enum Resource {
NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE,
EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE),
MOBILE_APP_SETTINGS,
CALCULATED_FIELD(EntityType.CALCULATED_FIELD);
CALCULATED_FIELD(EntityType.CALCULATED_FIELD),
AI_SETTINGS(EntityType.AI_SETTINGS);
private final Set<EntityType> entityTypes;

17
application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java

@ -18,6 +18,8 @@ package org.thingsboard.server.service.security.permission;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiSettings;
import org.thingsboard.server.common.data.id.AiSettingsId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
@ -56,6 +58,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.MOBILE_APP, tenantEntityPermissionChecker);
put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker);
put(Resource.CALCULATED_FIELD, tenantEntityPermissionChecker);
put(Resource.AI_SETTINGS, aiSettingsPermissionChecker);
}
public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() {
@ -146,4 +149,18 @@ public class TenantAdminPermissions extends AbstractPermissions {
};
private static final PermissionChecker<AiSettingsId, AiSettings> aiSettingsPermissionChecker = new PermissionChecker<>() {
@Override
public boolean hasPermission(SecurityUser user, Operation operation) {
return true;
}
@Override
public boolean hasPermission(SecurityUser user, Operation operation, AiSettingsId entityId, AiSettings entity) {
return user.getTenantId().equals(entity.getTenantId());
}
};
}

2
common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java

@ -26,7 +26,7 @@ import java.util.Optional;
public interface AiSettingsService extends EntityDaoService {
AiSettings save(TenantId tenantId, AiSettings aiSettings);
AiSettings save(AiSettings aiSettings);
Optional<AiSettings> findAiSettingsById(TenantId tenantId, AiSettingsId aiSettingsId);

6
common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java

@ -47,7 +47,7 @@ public final class AiSettings extends BaseData<AiSettingsId> implements HasTenan
TenantId tenantId;
@Schema(
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_ONLY,
description = "Version of the AI settings; increments automatically whenever the settings are changed",
example = "7",
@ -66,7 +66,7 @@ public final class AiSettings extends BaseData<AiSettingsId> implements HasTenan
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE,
description = "Name of the LLM provider, e.g. 'openai', 'anthropic'",
description = "Name of the LLM provider",
example = "openai"
)
String provider;
@ -74,7 +74,7 @@ public final class AiSettings extends BaseData<AiSettingsId> implements HasTenan
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE,
description = "Identifier of the LLM model to use, e.g. 'gpt-4o-mini'",
description = "Identifier of the LLM model to use",
example = "gpt-4o-mini"
)
String model;

8
dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java

@ -28,6 +28,8 @@ import org.thingsboard.server.common.data.page.PageLink;
import java.util.Optional;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@Service
@RequiredArgsConstructor
class AiSettingsServiceImpl implements AiSettingsService {
@ -35,9 +37,8 @@ class AiSettingsServiceImpl implements AiSettingsService {
private final AiSettingsDao aiSettingsDao;
@Override
public AiSettings save(TenantId tenantId, AiSettings aiSettings) {
aiSettings.setTenantId(tenantId);
return aiSettingsDao.saveAndFlush(tenantId, aiSettings);
public AiSettings save(AiSettings aiSettings) {
return aiSettingsDao.saveAndFlush(aiSettings.getTenantId(), aiSettings);
}
@Override
@ -47,6 +48,7 @@ class AiSettingsServiceImpl implements AiSettingsService {
@Override
public PageData<AiSettings> findAiSettingsByTenantId(TenantId tenantId, PageLink pageLink) {
validatePageLink(pageLink);
return aiSettingsDao.findAllByTenantId(tenantId, pageLink);
}

14
dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java

@ -15,23 +15,25 @@
*/
package org.thingsboard.server.dao.sql.ai;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.edqs.fields.AiSettingsFields;
import org.thingsboard.server.dao.model.sql.AiSettingsEntity;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface AiSettingsRepository extends JpaRepository<AiSettingsEntity, UUID> {
Page<AiSettingsEntity> findByTenantId(UUID tenantId, Pageable pageable);
@Query("SELECT ai " +
"FROM AiSettingsEntity ai " +
"WHERE ai.tenantId = :tenantId " +
"AND (:textSearch IS NULL OR ilike(ai.name, CONCAT('%', :textSearch, '%')) = true)")
Page<AiSettingsEntity> findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable);
Optional<AiSettingsEntity> findByTenantIdAndId(UUID tenantId, UUID id);
@ -41,6 +43,8 @@ public interface AiSettingsRepository extends JpaRepository<AiSettingsEntity, UU
void deleteByTenantId(UUID tenantId);
@Transactional
boolean deleteByTenantIdAndId(UUID tenantId, UUID id);
@Modifying
@Query("DELETE FROM AiSettingsEntity ai WHERE ai.tenantId = :tenantId AND ai.id = :id")
int deleteByTenantIdAndId(@Param("tenantId") UUID tenantId, @Param("id") UUID id);
}

15
dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java

@ -16,12 +16,11 @@
package org.thingsboard.server.dao.sql.ai;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Limit;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ai.AiSettings;
import org.thingsboard.server.common.data.edqs.fields.AiSettingsFields;
import org.thingsboard.server.common.data.id.AiSettingsId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@ -32,7 +31,6 @@ import org.thingsboard.server.dao.model.sql.AiSettingsEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@ -48,14 +46,11 @@ class JpaAiSettingsDao extends JpaAbstractDao<AiSettingsEntity, AiSettings> impl
return aiSettingsRepository.findByTenantIdAndId(tenantId.getId(), aiSettingsId.getId()).map(DaoUtil::getData);
}
@Override
public List<AiSettingsFields> findNextBatch(UUID id, int batchSize) {
return aiSettingsRepository.findNextBatch(id, Limit.of(batchSize));
}
@Override
public PageData<AiSettings> findAllByTenantId(TenantId tenantId, PageLink pageLink) {
return DaoUtil.toPageData(aiSettingsRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink)));
return DaoUtil.toPageData(aiSettingsRepository.findByTenantId(
tenantId.getId(), StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), DaoUtil.toPageable(pageLink))
);
}
@Override
@ -70,7 +65,7 @@ class JpaAiSettingsDao extends JpaAbstractDao<AiSettingsEntity, AiSettings> impl
@Override
public boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) {
return aiSettingsRepository.deleteByTenantIdAndId(tenantId.getId(), aiSettingsId.getId());
return aiSettingsRepository.deleteByTenantIdAndId(tenantId.getId(), aiSettingsId.getId()) > 0;
}
@Override

Loading…
Cancel
Save