AI rule node: draft implementation
This commit is contained in:
parent
55db24f7e8
commit
0f17e5f457
@ -377,6 +377,18 @@
|
|||||||
<groupId>org.rocksdb</groupId>
|
<groupId>org.rocksdb</groupId>
|
||||||
<artifactId>rocksdbjni</artifactId>
|
<artifactId>rocksdbjni</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-open-ai</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-anthropic</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-google-ai-gemini</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@ -14,3 +14,13 @@
|
|||||||
-- limitations under the License.
|
-- limitations under the License.
|
||||||
--
|
--
|
||||||
|
|
||||||
|
CREATE TABLE ai_settings (
|
||||||
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
|
created_time BIGINT NOT NULL,
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
version BIGINT NOT NULL DEFAULT 1,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
provider VARCHAR(255) NOT NULL,
|
||||||
|
model VARCHAR(255) NOT NULL,
|
||||||
|
api_key VARCHAR(1000) NOT NULL
|
||||||
|
);
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import org.thingsboard.rule.engine.api.DeviceStateManager;
|
|||||||
import org.thingsboard.rule.engine.api.MailService;
|
import org.thingsboard.rule.engine.api.MailService;
|
||||||
import org.thingsboard.rule.engine.api.MqttClientSettings;
|
import org.thingsboard.rule.engine.api.MqttClientSettings;
|
||||||
import org.thingsboard.rule.engine.api.NotificationCenter;
|
import org.thingsboard.rule.engine.api.NotificationCenter;
|
||||||
|
import org.thingsboard.rule.engine.api.RuleEngineAiService;
|
||||||
import org.thingsboard.rule.engine.api.SmsService;
|
import org.thingsboard.rule.engine.api.SmsService;
|
||||||
import org.thingsboard.rule.engine.api.notification.SlackService;
|
import org.thingsboard.rule.engine.api.notification.SlackService;
|
||||||
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
|
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
|
||||||
@ -309,6 +310,10 @@ public class ActorSystemContext {
|
|||||||
@Getter
|
@Getter
|
||||||
private AuditLogService auditLogService;
|
private AuditLogService auditLogService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Getter
|
||||||
|
private RuleEngineAiService aiService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Getter
|
@Getter
|
||||||
private EntityViewService entityViewService;
|
private EntityViewService entityViewService;
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import org.thingsboard.rule.engine.api.DeviceStateManager;
|
|||||||
import org.thingsboard.rule.engine.api.MailService;
|
import org.thingsboard.rule.engine.api.MailService;
|
||||||
import org.thingsboard.rule.engine.api.MqttClientSettings;
|
import org.thingsboard.rule.engine.api.MqttClientSettings;
|
||||||
import org.thingsboard.rule.engine.api.NotificationCenter;
|
import org.thingsboard.rule.engine.api.NotificationCenter;
|
||||||
|
import org.thingsboard.rule.engine.api.RuleEngineAiService;
|
||||||
import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
|
import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
|
||||||
import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService;
|
import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService;
|
||||||
import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache;
|
import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache;
|
||||||
@ -1012,6 +1013,11 @@ public class DefaultTbContext implements TbContext {
|
|||||||
return mainCtx.getAuditLogService();
|
return mainCtx.getAuditLogService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RuleEngineAiService getAiService() {
|
||||||
|
return mainCtx.getAiService();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MqttClientSettings getMqttClientSettings() {
|
public MqttClientSettings getMqttClientSettings() {
|
||||||
return mainCtx.getMqttClientSettings();
|
return mainCtx.getMqttClientSettings();
|
||||||
|
|||||||
@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2025 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.controller;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.thingsboard.server.common.data.ai.AiSettings;
|
||||||
|
import org.thingsboard.server.common.data.exception.ThingsboardException;
|
||||||
|
import org.thingsboard.server.common.data.id.AiSettingsId;
|
||||||
|
import org.thingsboard.server.common.data.page.PageData;
|
||||||
|
import org.thingsboard.server.common.data.page.PageLink;
|
||||||
|
import org.thingsboard.server.dao.ai.AiSettingsService;
|
||||||
|
import org.thingsboard.server.service.security.model.SecurityUser;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
// TODO: TbAiSettingsService?
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("/api/ai-settings")
|
||||||
|
public class AiSettingsController extends BaseController {
|
||||||
|
|
||||||
|
private final AiSettingsService aiSettingsService;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public AiSettings saveAiSettings(
|
||||||
|
@RequestBody AiSettings aiSettings,
|
||||||
|
|
||||||
|
@AuthenticationPrincipal SecurityUser requestingUser
|
||||||
|
) {
|
||||||
|
return aiSettingsService.save(requestingUser.getTenantId(), aiSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{aiSettingsId}")
|
||||||
|
public AiSettings getAiSettingsById(
|
||||||
|
@PathVariable("aiSettingsId") UUID aiSettingsUuid,
|
||||||
|
|
||||||
|
@AuthenticationPrincipal SecurityUser requestingUser
|
||||||
|
) throws ThingsboardException {
|
||||||
|
return checkNotNull(aiSettingsService.findAiSettingsByTenantIdAndId(requestingUser.getTenantId(), new AiSettingsId(aiSettingsUuid)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public PageData<AiSettings> getAllAiSettings(
|
||||||
|
@AuthenticationPrincipal SecurityUser requestingUser
|
||||||
|
) {
|
||||||
|
return aiSettingsService.findAiSettingsByTenantId(requestingUser.getTenantId(), new PageLink(Integer.MAX_VALUE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{aiSettingsId}")
|
||||||
|
public boolean deleteAiSettingsById(
|
||||||
|
@PathVariable("aiSettingsId") UUID aiSettingsUuid,
|
||||||
|
|
||||||
|
@AuthenticationPrincipal SecurityUser requestingUser
|
||||||
|
) {
|
||||||
|
return aiSettingsService.deleteByTenantIdAndId(requestingUser.getTenantId(), new AiSettingsId(aiSettingsUuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2025 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.service.ai;
|
||||||
|
|
||||||
|
import dev.langchain4j.model.anthropic.AnthropicChatModel;
|
||||||
|
import dev.langchain4j.model.chat.ChatModel;
|
||||||
|
import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel;
|
||||||
|
import dev.langchain4j.model.openai.OpenAiChatModel;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.thingsboard.rule.engine.api.RuleEngineAiService;
|
||||||
|
import org.thingsboard.server.common.data.ai.AiSettings;
|
||||||
|
import org.thingsboard.server.common.data.id.AiSettingsId;
|
||||||
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
import org.thingsboard.server.dao.ai.AiSettingsService;
|
||||||
|
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
class AiServiceImpl implements RuleEngineAiService {
|
||||||
|
|
||||||
|
private final AiSettingsService aiSettingsService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChatModel configureChatModel(TenantId tenantId, AiSettingsId aiSettingsId) {
|
||||||
|
Optional<AiSettings> aiSettingsOpt = aiSettingsService.findAiSettingsById(tenantId, aiSettingsId);
|
||||||
|
if (aiSettingsOpt.isEmpty()) {
|
||||||
|
throw new NoSuchElementException("AI settings with ID: " + aiSettingsId + " were not found");
|
||||||
|
}
|
||||||
|
var aiSettings = aiSettingsOpt.get();
|
||||||
|
|
||||||
|
return switch (aiSettings.getProvider()) {
|
||||||
|
case "openai" -> OpenAiChatModel.builder()
|
||||||
|
.apiKey(aiSettings.getApiKey())
|
||||||
|
.modelName(aiSettings.getModel())
|
||||||
|
.build();
|
||||||
|
case "anthropic" -> AnthropicChatModel.builder()
|
||||||
|
.apiKey(aiSettings.getApiKey())
|
||||||
|
.modelName(aiSettings.getModel())
|
||||||
|
.build();
|
||||||
|
case "google-ai-gemini" -> GoogleAiGeminiChatModel.builder()
|
||||||
|
.apiKey(aiSettings.getApiKey())
|
||||||
|
.modelName(aiSettings.getModel())
|
||||||
|
.build();
|
||||||
|
default -> throw new IllegalArgumentException("Unsupported AI provider: " + aiSettings.getProvider());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2025 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.dao.ai;
|
||||||
|
|
||||||
|
import org.thingsboard.server.common.data.ai.AiSettings;
|
||||||
|
import org.thingsboard.server.common.data.id.AiSettingsId;
|
||||||
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
import org.thingsboard.server.common.data.page.PageData;
|
||||||
|
import org.thingsboard.server.common.data.page.PageLink;
|
||||||
|
import org.thingsboard.server.dao.entity.EntityDaoService;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface AiSettingsService extends EntityDaoService {
|
||||||
|
|
||||||
|
AiSettings save(TenantId tenantId, AiSettings aiSettings);
|
||||||
|
|
||||||
|
Optional<AiSettings> findAiSettingsById(TenantId tenantId, AiSettingsId aiSettingsId);
|
||||||
|
|
||||||
|
PageData<AiSettings> findAiSettingsByTenantId(TenantId tenantId, PageLink pageLink);
|
||||||
|
|
||||||
|
Optional<AiSettings> findAiSettingsByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId);
|
||||||
|
|
||||||
|
boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId);
|
||||||
|
|
||||||
|
}
|
||||||
@ -16,6 +16,7 @@
|
|||||||
package org.thingsboard.server.common.data;
|
package org.thingsboard.server.common.data;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import org.thingsboard.server.common.data.id.IdBased;
|
import org.thingsboard.server.common.data.id.IdBased;
|
||||||
import org.thingsboard.server.common.data.id.UUIDBased;
|
import org.thingsboard.server.common.data.id.UUIDBased;
|
||||||
|
|
||||||
@ -41,6 +42,11 @@ public abstract class BaseData<I extends UUIDBased> extends IdBased<I> implement
|
|||||||
this.createdTime = data.getCreatedTime();
|
this.createdTime = data.getCreatedTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Entity creation timestamp in milliseconds since Unix epoch",
|
||||||
|
example = "1746028547220",
|
||||||
|
accessMode = Schema.AccessMode.READ_ONLY
|
||||||
|
)
|
||||||
public long getCreatedTime() {
|
public long getCreatedTime() {
|
||||||
return createdTime;
|
return createdTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,7 +63,13 @@ public enum EntityType {
|
|||||||
MOBILE_APP(37),
|
MOBILE_APP(37),
|
||||||
MOBILE_APP_BUNDLE(38),
|
MOBILE_APP_BUNDLE(38),
|
||||||
CALCULATED_FIELD(39),
|
CALCULATED_FIELD(39),
|
||||||
CALCULATED_FIELD_LINK(40);
|
CALCULATED_FIELD_LINK(40),
|
||||||
|
AI_SETTINGS(41, "ai_settings") {
|
||||||
|
@Override
|
||||||
|
public String getNormalName() {
|
||||||
|
return "AI settings";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private final int protoNumber; // Corresponds to EntityTypeProto
|
private final int protoNumber; // Corresponds to EntityTypeProto
|
||||||
|
|||||||
@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2025 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.common.data.ai;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.thingsboard.server.common.data.BaseData;
|
||||||
|
import org.thingsboard.server.common.data.HasName;
|
||||||
|
import org.thingsboard.server.common.data.HasTenantId;
|
||||||
|
import org.thingsboard.server.common.data.HasVersion;
|
||||||
|
import org.thingsboard.server.common.data.id.AiSettingsId;
|
||||||
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public final class AiSettings extends BaseData<AiSettingsId> implements HasTenantId, HasVersion, HasName {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 9017108678716011604L;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
accessMode = Schema.AccessMode.READ_ONLY,
|
||||||
|
description = "JSON object representing the ID of the tenant associated with these AI settings",
|
||||||
|
example = "e3c4b7d2-5678-4a9b-0c1d-2e3f4a5b6c7d"
|
||||||
|
)
|
||||||
|
TenantId tenantId;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
|
||||||
|
accessMode = Schema.AccessMode.READ_ONLY,
|
||||||
|
description = "Version of the AI settings; increments automatically whenever the settings are changed",
|
||||||
|
example = "7",
|
||||||
|
defaultValue = "1"
|
||||||
|
)
|
||||||
|
Long version;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
accessMode = Schema.AccessMode.READ_WRITE,
|
||||||
|
description = "Human-readable name of the AI settings",
|
||||||
|
example = "Default AI Settings"
|
||||||
|
)
|
||||||
|
String name;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
accessMode = Schema.AccessMode.READ_WRITE,
|
||||||
|
description = "Name of the LLM provider, e.g. 'openai', 'anthropic'",
|
||||||
|
example = "openai"
|
||||||
|
)
|
||||||
|
String provider;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
accessMode = Schema.AccessMode.READ_WRITE,
|
||||||
|
description = "Identifier of the LLM model to use, e.g. 'gpt-4o-mini'",
|
||||||
|
example = "gpt-4o-mini"
|
||||||
|
)
|
||||||
|
String model;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
accessMode = Schema.AccessMode.WRITE_ONLY,
|
||||||
|
description = "API key for authenticating with the selected LLM provider",
|
||||||
|
example = "sk-********************************"
|
||||||
|
)
|
||||||
|
String apiKey;
|
||||||
|
|
||||||
|
public AiSettings(AiSettingsId id) {
|
||||||
|
super(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2025 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.common.data.edqs.fields;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class AiSettingsFields extends AbstractEntityFields {
|
||||||
|
|
||||||
|
private String provider;
|
||||||
|
private String model;
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
|
public AiSettingsFields(UUID id, long createdTime, UUID tenantId, long version, String provider, String name, String model, String apiKey) {
|
||||||
|
super(id, createdTime, tenantId, name, version);
|
||||||
|
this.provider = provider;
|
||||||
|
this.model = model;
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2025 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.common.data.id;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.thingsboard.server.common.data.EntityType;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class AiSettingsId extends UUIDBased implements EntityId {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 3021036138554389754L;
|
||||||
|
|
||||||
|
@JsonCreator
|
||||||
|
public AiSettingsId(@JsonProperty("id") UUID id) {
|
||||||
|
super(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Schema(
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
description = "Entity type of the AI settings, always 'AI_SETTINGS'",
|
||||||
|
example = "AI_SETTINGS",
|
||||||
|
allowableValues = "AI_SETTINGS"
|
||||||
|
)
|
||||||
|
public EntityType getEntityType() {
|
||||||
|
return EntityType.AI_SETTINGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AiSettingsId fromString(String uuid) {
|
||||||
|
return new AiSettingsId(UUID.fromString(uuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -50,75 +50,43 @@ public class EntityIdFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static EntityId getByTypeAndUuid(EntityType type, UUID uuid) {
|
public static EntityId getByTypeAndUuid(EntityType type, UUID uuid) {
|
||||||
switch (type) {
|
return switch (type) {
|
||||||
case TENANT:
|
case TENANT -> TenantId.fromUUID(uuid);
|
||||||
return TenantId.fromUUID(uuid);
|
case CUSTOMER -> new CustomerId(uuid);
|
||||||
case CUSTOMER:
|
case USER -> new UserId(uuid);
|
||||||
return new CustomerId(uuid);
|
case DASHBOARD -> new DashboardId(uuid);
|
||||||
case USER:
|
case DEVICE -> new DeviceId(uuid);
|
||||||
return new UserId(uuid);
|
case ASSET -> new AssetId(uuid);
|
||||||
case DASHBOARD:
|
case ALARM -> new AlarmId(uuid);
|
||||||
return new DashboardId(uuid);
|
case RULE_CHAIN -> new RuleChainId(uuid);
|
||||||
case DEVICE:
|
case RULE_NODE -> new RuleNodeId(uuid);
|
||||||
return new DeviceId(uuid);
|
case ENTITY_VIEW -> new EntityViewId(uuid);
|
||||||
case ASSET:
|
case WIDGETS_BUNDLE -> new WidgetsBundleId(uuid);
|
||||||
return new AssetId(uuid);
|
case WIDGET_TYPE -> new WidgetTypeId(uuid);
|
||||||
case ALARM:
|
case DEVICE_PROFILE -> new DeviceProfileId(uuid);
|
||||||
return new AlarmId(uuid);
|
case ASSET_PROFILE -> new AssetProfileId(uuid);
|
||||||
case RULE_CHAIN:
|
case TENANT_PROFILE -> new TenantProfileId(uuid);
|
||||||
return new RuleChainId(uuid);
|
case API_USAGE_STATE -> new ApiUsageStateId(uuid);
|
||||||
case RULE_NODE:
|
case TB_RESOURCE -> new TbResourceId(uuid);
|
||||||
return new RuleNodeId(uuid);
|
case OTA_PACKAGE -> new OtaPackageId(uuid);
|
||||||
case ENTITY_VIEW:
|
case EDGE -> new EdgeId(uuid);
|
||||||
return new EntityViewId(uuid);
|
case RPC -> new RpcId(uuid);
|
||||||
case WIDGETS_BUNDLE:
|
case QUEUE -> new QueueId(uuid);
|
||||||
return new WidgetsBundleId(uuid);
|
case NOTIFICATION_TARGET -> new NotificationTargetId(uuid);
|
||||||
case WIDGET_TYPE:
|
case NOTIFICATION_REQUEST -> new NotificationRequestId(uuid);
|
||||||
return new WidgetTypeId(uuid);
|
case NOTIFICATION_RULE -> new NotificationRuleId(uuid);
|
||||||
case DEVICE_PROFILE:
|
case NOTIFICATION_TEMPLATE -> new NotificationTemplateId(uuid);
|
||||||
return new DeviceProfileId(uuid);
|
case NOTIFICATION -> new NotificationId(uuid);
|
||||||
case ASSET_PROFILE:
|
case QUEUE_STATS -> new QueueStatsId(uuid);
|
||||||
return new AssetProfileId(uuid);
|
case OAUTH2_CLIENT -> new OAuth2ClientId(uuid);
|
||||||
case TENANT_PROFILE:
|
case MOBILE_APP -> new MobileAppId(uuid);
|
||||||
return new TenantProfileId(uuid);
|
case DOMAIN -> new DomainId(uuid);
|
||||||
case API_USAGE_STATE:
|
case MOBILE_APP_BUNDLE -> new MobileAppBundleId(uuid);
|
||||||
return new ApiUsageStateId(uuid);
|
case CALCULATED_FIELD -> new CalculatedFieldId(uuid);
|
||||||
case TB_RESOURCE:
|
case CALCULATED_FIELD_LINK -> new CalculatedFieldLinkId(uuid);
|
||||||
return new TbResourceId(uuid);
|
case AI_SETTINGS -> new AiSettingsId(uuid);
|
||||||
case OTA_PACKAGE:
|
default -> throw new IllegalArgumentException("EntityType " + type + " is not supported!");
|
||||||
return new OtaPackageId(uuid);
|
};
|
||||||
case EDGE:
|
|
||||||
return new EdgeId(uuid);
|
|
||||||
case RPC:
|
|
||||||
return new RpcId(uuid);
|
|
||||||
case QUEUE:
|
|
||||||
return new QueueId(uuid);
|
|
||||||
case NOTIFICATION_TARGET:
|
|
||||||
return new NotificationTargetId(uuid);
|
|
||||||
case NOTIFICATION_REQUEST:
|
|
||||||
return new NotificationRequestId(uuid);
|
|
||||||
case NOTIFICATION_RULE:
|
|
||||||
return new NotificationRuleId(uuid);
|
|
||||||
case NOTIFICATION_TEMPLATE:
|
|
||||||
return new NotificationTemplateId(uuid);
|
|
||||||
case NOTIFICATION:
|
|
||||||
return new NotificationId(uuid);
|
|
||||||
case QUEUE_STATS:
|
|
||||||
return new QueueStatsId(uuid);
|
|
||||||
case OAUTH2_CLIENT:
|
|
||||||
return new OAuth2ClientId(uuid);
|
|
||||||
case MOBILE_APP:
|
|
||||||
return new MobileAppId(uuid);
|
|
||||||
case DOMAIN:
|
|
||||||
return new DomainId(uuid);
|
|
||||||
case MOBILE_APP_BUNDLE:
|
|
||||||
return new MobileAppBundleId(uuid);
|
|
||||||
case CALCULATED_FIELD:
|
|
||||||
return new CalculatedFieldId(uuid);
|
|
||||||
case CALCULATED_FIELD_LINK:
|
|
||||||
return new CalculatedFieldLinkId(uuid);
|
|
||||||
}
|
|
||||||
throw new IllegalArgumentException("EntityType " + type + " is not supported!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static EntityId getByEdgeEventTypeAndUuid(EdgeEventType edgeEventType, UUID uuid) {
|
public static EntityId getByEdgeEventTypeAndUuid(EdgeEventType edgeEventType, UUID uuid) {
|
||||||
|
|||||||
@ -63,6 +63,7 @@ enum EntityTypeProto {
|
|||||||
MOBILE_APP_BUNDLE = 38;
|
MOBILE_APP_BUNDLE = 38;
|
||||||
CALCULATED_FIELD = 39;
|
CALCULATED_FIELD = 39;
|
||||||
CALCULATED_FIELD_LINK = 40;
|
CALCULATED_FIELD_LINK = 40;
|
||||||
|
AI_SETTINGS = 41;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ApiUsageRecordKeyProto {
|
enum ApiUsageRecordKeyProto {
|
||||||
|
|||||||
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2025 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.dao.ai;
|
||||||
|
|
||||||
|
import org.thingsboard.server.common.data.ai.AiSettings;
|
||||||
|
import org.thingsboard.server.common.data.id.AiSettingsId;
|
||||||
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
import org.thingsboard.server.dao.Dao;
|
||||||
|
import org.thingsboard.server.dao.TenantEntityDao;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface AiSettingsDao extends Dao<AiSettings>, TenantEntityDao<AiSettings> {
|
||||||
|
|
||||||
|
Optional<AiSettings> findByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId);
|
||||||
|
|
||||||
|
void deleteByTenantId(TenantId tenantId);
|
||||||
|
|
||||||
|
boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId);
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2025 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.dao.ai;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.thingsboard.server.common.data.EntityType;
|
||||||
|
import org.thingsboard.server.common.data.ai.AiSettings;
|
||||||
|
import org.thingsboard.server.common.data.id.AiSettingsId;
|
||||||
|
import org.thingsboard.server.common.data.id.EntityId;
|
||||||
|
import org.thingsboard.server.common.data.id.HasId;
|
||||||
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
import org.thingsboard.server.common.data.page.PageData;
|
||||||
|
import org.thingsboard.server.common.data.page.PageLink;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
class AiSettingsServiceImpl implements AiSettingsService {
|
||||||
|
|
||||||
|
private final AiSettingsDao aiSettingsDao;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AiSettings save(TenantId tenantId, AiSettings aiSettings) {
|
||||||
|
aiSettings.setTenantId(tenantId);
|
||||||
|
return aiSettingsDao.saveAndFlush(tenantId, aiSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<AiSettings> findAiSettingsById(TenantId tenantId, AiSettingsId aiSettingsId) {
|
||||||
|
return Optional.ofNullable(aiSettingsDao.findById(tenantId, aiSettingsId.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageData<AiSettings> findAiSettingsByTenantId(TenantId tenantId, PageLink pageLink) {
|
||||||
|
return aiSettingsDao.findAllByTenantId(tenantId, pageLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<AiSettings> findAiSettingsByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) {
|
||||||
|
return aiSettingsDao.findByTenantIdAndId(tenantId, aiSettingsId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) {
|
||||||
|
return aiSettingsDao.deleteByTenantIdAndId(tenantId, aiSettingsId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) {
|
||||||
|
return Optional.ofNullable(aiSettingsDao.findById(tenantId, entityId.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long countByTenantId(TenantId tenantId) {
|
||||||
|
return aiSettingsDao.countByTenantId(tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
|
||||||
|
aiSettingsDao.removeById(tenantId, id.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteByTenantId(TenantId tenantId) {
|
||||||
|
aiSettingsDao.deleteByTenantId(tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EntityType getEntityType() {
|
||||||
|
return EntityType.AI_SETTINGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -739,6 +739,16 @@ public class ModelConstants {
|
|||||||
public static final String CALCULATED_FIELD_LINK_ENTITY_ID = ENTITY_ID_COLUMN;
|
public static final String CALCULATED_FIELD_LINK_ENTITY_ID = ENTITY_ID_COLUMN;
|
||||||
public static final String CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID = "calculated_field_id";
|
public static final String CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID = "calculated_field_id";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI settings constants.
|
||||||
|
*/
|
||||||
|
public static final String AI_SETTINGS_TABLE_NAME = "ai_settings";
|
||||||
|
public static final String AI_SETTINGS_TENANT_ID_COLUMN_NAME = TENANT_ID_COLUMN;
|
||||||
|
public static final String AI_SETTINGS_NAME_COLUMN_NAME = NAME_PROPERTY;
|
||||||
|
public static final String AI_SETTINGS_PROVIDER_COLUMN_NAME = "provider";
|
||||||
|
public static final String AI_SETTINGS_MODEL_COLUMN_NAME = "model";
|
||||||
|
public static final String AI_SETTINGS_API_KEY_COLUMN_NAME = "api_key";
|
||||||
|
|
||||||
protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN};
|
protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN};
|
||||||
|
|
||||||
protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN), max(TS_COLUMN)};
|
protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN), max(TS_COLUMN)};
|
||||||
|
|||||||
@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2025 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.dao.model.sql;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
import org.hibernate.proxy.HibernateProxy;
|
||||||
|
import org.thingsboard.server.common.data.ai.AiSettings;
|
||||||
|
import org.thingsboard.server.common.data.id.AiSettingsId;
|
||||||
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
import org.thingsboard.server.dao.model.BaseVersionedEntity;
|
||||||
|
import org.thingsboard.server.dao.model.ModelConstants;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@ToString
|
||||||
|
@Entity
|
||||||
|
@Table(name = ModelConstants.AI_SETTINGS_TABLE_NAME)
|
||||||
|
public class AiSettingsEntity extends BaseVersionedEntity<AiSettings> {
|
||||||
|
|
||||||
|
@Column(name = ModelConstants.AI_SETTINGS_TENANT_ID_COLUMN_NAME, nullable = false, columnDefinition = "uuid")
|
||||||
|
private UUID tenantId;
|
||||||
|
|
||||||
|
@Column(name = ModelConstants.AI_SETTINGS_NAME_COLUMN_NAME, nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(name = ModelConstants.AI_SETTINGS_PROVIDER_COLUMN_NAME, nullable = false)
|
||||||
|
private String provider;
|
||||||
|
|
||||||
|
@Column(name = ModelConstants.AI_SETTINGS_MODEL_COLUMN_NAME, nullable = false)
|
||||||
|
private String model;
|
||||||
|
|
||||||
|
@Column(name = ModelConstants.AI_SETTINGS_API_KEY_COLUMN_NAME, nullable = false)
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
|
public AiSettingsEntity() {}
|
||||||
|
|
||||||
|
public AiSettingsEntity(AiSettings aiSettings) {
|
||||||
|
super(aiSettings);
|
||||||
|
tenantId = getTenantUuid(aiSettings.getTenantId());
|
||||||
|
name = aiSettings.getName();
|
||||||
|
provider = aiSettings.getProvider();
|
||||||
|
model = aiSettings.getModel();
|
||||||
|
apiKey = aiSettings.getApiKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AiSettings toData() {
|
||||||
|
var settings = new AiSettings(new AiSettingsId(id));
|
||||||
|
settings.setCreatedTime(createdTime);
|
||||||
|
settings.setVersion(version);
|
||||||
|
settings.setTenantId(TenantId.fromUUID(tenantId));
|
||||||
|
settings.setName(name);
|
||||||
|
settings.setProvider(provider);
|
||||||
|
settings.setModel(model);
|
||||||
|
settings.setApiKey(apiKey);
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null) return false;
|
||||||
|
Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
|
||||||
|
Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
|
||||||
|
if (thisEffectiveClass != oEffectiveClass) return false;
|
||||||
|
AiSettingsEntity that = (AiSettingsEntity) o;
|
||||||
|
return getId() != null && Objects.equals(getId(), that.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final int hashCode() {
|
||||||
|
return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2025 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.dao.sql.ai;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Limit;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.thingsboard.server.common.data.edqs.fields.AiSettingsFields;
|
||||||
|
import org.thingsboard.server.dao.model.sql.AiSettingsEntity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface AiSettingsRepository extends JpaRepository<AiSettingsEntity, UUID> {
|
||||||
|
|
||||||
|
Page<AiSettingsEntity> findByTenantId(UUID tenantId, Pageable pageable);
|
||||||
|
|
||||||
|
Optional<AiSettingsEntity> findByTenantIdAndId(UUID tenantId, UUID id);
|
||||||
|
|
||||||
|
@Query("SELECT new org.thingsboard.server.common.data.edqs.fields.AiSettingsFields(" +
|
||||||
|
"ai.id, ai.createdTime, ai.tenantId, ai.version, ai.provider, ai.name, ai.model, ai.apiKey) " +
|
||||||
|
"FROM AiSettingsEntity ai WHERE ai.id > :id ORDER BY ai.id")
|
||||||
|
List<AiSettingsFields> findNextBatch(@Param("id") UUID id, Limit limit);
|
||||||
|
|
||||||
|
Long countByTenantId(UUID tenantId);
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
void deleteByTenantId(UUID tenantId);
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
boolean deleteByTenantIdAndId(UUID tenantId, UUID id);
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2025 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.dao.sql.ai;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Limit;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.thingsboard.server.common.data.EntityType;
|
||||||
|
import org.thingsboard.server.common.data.ai.AiSettings;
|
||||||
|
import org.thingsboard.server.common.data.edqs.fields.AiSettingsFields;
|
||||||
|
import org.thingsboard.server.common.data.id.AiSettingsId;
|
||||||
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
import org.thingsboard.server.common.data.page.PageData;
|
||||||
|
import org.thingsboard.server.common.data.page.PageLink;
|
||||||
|
import org.thingsboard.server.dao.DaoUtil;
|
||||||
|
import org.thingsboard.server.dao.ai.AiSettingsDao;
|
||||||
|
import org.thingsboard.server.dao.model.sql.AiSettingsEntity;
|
||||||
|
import org.thingsboard.server.dao.sql.JpaAbstractDao;
|
||||||
|
import org.thingsboard.server.dao.util.SqlDao;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@SqlDao
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
class JpaAiSettingsDao extends JpaAbstractDao<AiSettingsEntity, AiSettings> implements AiSettingsDao {
|
||||||
|
|
||||||
|
private final AiSettingsRepository aiSettingsRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<AiSettings> findByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) {
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long countByTenantId(TenantId tenantId) {
|
||||||
|
return aiSettingsRepository.countByTenantId(tenantId.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteByTenantId(TenantId tenantId) {
|
||||||
|
aiSettingsRepository.deleteByTenantId(tenantId.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) {
|
||||||
|
return aiSettingsRepository.deleteByTenantIdAndId(tenantId.getId(), aiSettingsId.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EntityType getEntityType() {
|
||||||
|
return EntityType.AI_SETTINGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Class<AiSettingsEntity> getEntityClass() {
|
||||||
|
return AiSettingsEntity.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected JpaRepository<AiSettingsEntity, UUID> getRepository() {
|
||||||
|
return aiSettingsRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -948,3 +948,14 @@ CREATE TABLE IF NOT EXISTS cf_debug_event (
|
|||||||
e_result varchar,
|
e_result varchar,
|
||||||
e_error varchar
|
e_error varchar
|
||||||
) PARTITION BY RANGE (ts);
|
) PARTITION BY RANGE (ts);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ai_settings (
|
||||||
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
|
created_time BIGINT NOT NULL,
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
version BIGINT NOT NULL DEFAULT 1,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
provider VARCHAR(255) NOT NULL,
|
||||||
|
model VARCHAR(255) NOT NULL,
|
||||||
|
api_key VARCHAR(1000) NOT NULL
|
||||||
|
);
|
||||||
|
|||||||
8
pom.xml
8
pom.xml
@ -134,6 +134,7 @@
|
|||||||
<antisamy.version>1.7.5</antisamy.version>
|
<antisamy.version>1.7.5</antisamy.version>
|
||||||
<snmp4j.version>3.8.0</snmp4j.version>
|
<snmp4j.version>3.8.0</snmp4j.version>
|
||||||
<json-path.version>2.9.0</json-path.version>
|
<json-path.version>2.9.0</json-path.version>
|
||||||
|
<langchain4j.version>1.0.0-beta4</langchain4j.version>
|
||||||
<!-- TEST SCOPE -->
|
<!-- TEST SCOPE -->
|
||||||
<awaitility.version>4.2.1</awaitility.version>
|
<awaitility.version>4.2.1</awaitility.version>
|
||||||
<dbunit.version>2.7.3</dbunit.version>
|
<dbunit.version>2.7.3</dbunit.version>
|
||||||
@ -2322,6 +2323,13 @@
|
|||||||
<artifactId>rocksdbjni</artifactId>
|
<artifactId>rocksdbjni</artifactId>
|
||||||
<version>${rocksdbjni.version}</version>
|
<version>${rocksdbjni.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-bom</artifactId>
|
||||||
|
<version>${langchain4j.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|||||||
@ -98,6 +98,10 @@
|
|||||||
<artifactId>jakarta.mail</artifactId>
|
<artifactId>jakarta.mail</artifactId>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2025 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.rule.engine.api;
|
||||||
|
|
||||||
|
import dev.langchain4j.model.chat.ChatModel;
|
||||||
|
import org.thingsboard.server.common.data.id.AiSettingsId;
|
||||||
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
|
||||||
|
public interface RuleEngineAiService {
|
||||||
|
|
||||||
|
ChatModel configureChatModel(TenantId tenantId, AiSettingsId aiSettingsId);
|
||||||
|
|
||||||
|
}
|
||||||
@ -417,6 +417,8 @@ public interface TbContext {
|
|||||||
|
|
||||||
AuditLogService getAuditLogService();
|
AuditLogService getAuditLogService();
|
||||||
|
|
||||||
|
RuleEngineAiService getAiService();
|
||||||
|
|
||||||
// Configuration parameters for the MQTT client that is used in the MQTT node and Azure IoT hub node
|
// Configuration parameters for the MQTT client that is used in the MQTT node and Azure IoT hub node
|
||||||
|
|
||||||
MqttClientSettings getMqttClientSettings();
|
MqttClientSettings getMqttClientSettings();
|
||||||
|
|||||||
@ -153,6 +153,10 @@
|
|||||||
<groupId>com.jayway.jsonpath</groupId>
|
<groupId>com.jayway.jsonpath</groupId>
|
||||||
<artifactId>json-path</artifactId>
|
<artifactId>json-path</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2025 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.rule.engine.ai;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import dev.langchain4j.data.message.SystemMessage;
|
||||||
|
import dev.langchain4j.data.message.UserMessage;
|
||||||
|
import dev.langchain4j.model.chat.ChatModel;
|
||||||
|
import dev.langchain4j.model.chat.request.ChatRequest;
|
||||||
|
import dev.langchain4j.model.chat.request.ResponseFormat;
|
||||||
|
import dev.langchain4j.model.input.PromptTemplate;
|
||||||
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
|
import org.thingsboard.common.util.JacksonUtil;
|
||||||
|
import org.thingsboard.rule.engine.api.RuleNode;
|
||||||
|
import org.thingsboard.rule.engine.api.TbContext;
|
||||||
|
import org.thingsboard.rule.engine.api.TbNode;
|
||||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
|
||||||
|
import org.thingsboard.rule.engine.api.TbNodeException;
|
||||||
|
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
|
||||||
|
import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
|
||||||
|
import org.thingsboard.server.common.data.plugin.ComponentType;
|
||||||
|
import org.thingsboard.server.common.msg.TbMsg;
|
||||||
|
import org.thingsboard.server.dao.exception.DataValidationException;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static com.google.common.util.concurrent.Futures.addCallback;
|
||||||
|
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
|
||||||
|
import static java.util.Objects.requireNonNullElse;
|
||||||
|
import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields;
|
||||||
|
|
||||||
|
@RuleNode(
|
||||||
|
type = ComponentType.EXTERNAL,
|
||||||
|
name = "AI",
|
||||||
|
nodeDescription = "Interact with AI",
|
||||||
|
nodeDetails = "This node makes requests to LLM based on a prompt and a input message and returns a response in a form of output message",
|
||||||
|
configClazz = TbAiNodeConfiguration.class
|
||||||
|
)
|
||||||
|
public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
|
||||||
|
|
||||||
|
private static final SystemMessage SYSTEM_MESSAGE = SystemMessage.from("""
|
||||||
|
Take a deep breath and work on this step by step.
|
||||||
|
You are an industry-leading IoT domain expert with deep experience in telemetry data analysis.
|
||||||
|
Your task is to complete the user-provided task or answer a question.
|
||||||
|
You may use additional context information called "Rule engine message payload", "Rule engine message metadata" and "Rule engine message type".
|
||||||
|
Your response must be in JSON format.""");
|
||||||
|
|
||||||
|
private TbAiNodeConfiguration config;
|
||||||
|
|
||||||
|
private PromptTemplate userPromptTemplate;
|
||||||
|
private ChatModel chatModel;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
|
||||||
|
config = TbNodeUtils.convert(configuration, TbAiNodeConfiguration.class);
|
||||||
|
String errorPrefix = "'" + ctx.getSelf().getName() + "' node configuration is invalid: ";
|
||||||
|
try {
|
||||||
|
validateFields(config, errorPrefix);
|
||||||
|
} catch (DataValidationException e) {
|
||||||
|
throw new TbNodeException(e, true);
|
||||||
|
}
|
||||||
|
userPromptTemplate = PromptTemplate.from("""
|
||||||
|
User-provided task or question: %s
|
||||||
|
Rule engine message payload: {{msgPayload}}
|
||||||
|
Rule engine message metadata: {{msgMetadata}}
|
||||||
|
Rule engine message type: {{msgType}}"""
|
||||||
|
.formatted(config.getUserPrompt())
|
||||||
|
);
|
||||||
|
chatModel = ctx.getAiService().configureChatModel(ctx.getTenantId(), config.getAiSettingsId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMsg(TbContext ctx, TbMsg msg) {
|
||||||
|
var ackedMsg = ackIfNeeded(ctx, msg);
|
||||||
|
|
||||||
|
Map<String, Object> variables = Map.of(
|
||||||
|
"msgPayload", msg.getData(),
|
||||||
|
"msgMetadata", requireNonNullElse(JacksonUtil.toString(msg.getMetaData().getData()), "{}"),
|
||||||
|
"msgType", msg.getType()
|
||||||
|
);
|
||||||
|
UserMessage userMessage = userPromptTemplate.apply(variables).toUserMessage();
|
||||||
|
|
||||||
|
var chatRequest = ChatRequest.builder()
|
||||||
|
.messages(List.of(SYSTEM_MESSAGE, userMessage))
|
||||||
|
.responseFormat(ResponseFormat.JSON)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
addCallback(sendChatRequest(ctx, chatRequest), new FutureCallback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(String response) {
|
||||||
|
tellSuccess(ctx, ackedMsg.transform()
|
||||||
|
.data(response)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Throwable t) {
|
||||||
|
tellFailure(ctx, ackedMsg, t);
|
||||||
|
}
|
||||||
|
}, directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListenableFuture<String> sendChatRequest(TbContext ctx, ChatRequest chatRequest) {
|
||||||
|
return ctx.getExternalCallExecutor().submit(() -> chatModel.chat(chatRequest).aiMessage().text());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
config = null;
|
||||||
|
userPromptTemplate = null;
|
||||||
|
chatModel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2025 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.rule.engine.ai;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.thingsboard.rule.engine.api.NodeConfiguration;
|
||||||
|
import org.thingsboard.server.common.data.id.AiSettingsId;
|
||||||
|
import org.thingsboard.server.common.data.validation.Length;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class TbAiNodeConfiguration implements NodeConfiguration<TbAiNodeConfiguration> {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private AiSettingsId aiSettingsId;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Length(min = 1, max = 1000)
|
||||||
|
private String userPrompt;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TbAiNodeConfiguration defaultConfiguration() {
|
||||||
|
var configuration = new TbAiNodeConfiguration();
|
||||||
|
configuration.setUserPrompt("Tell me a joke");
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user