28 changed files with 1048 additions and 70 deletions
@ -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); |
||||
|
|
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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); |
||||
|
|
||||
|
} |
||||
@ -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…
Reference in new issue