62 changed files with 901 additions and 879 deletions
@ -0,0 +1,37 @@ |
|||
/** |
|||
* 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.chat.ChatModel; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.rule.engine.api.RuleEngineAiModelService; |
|||
import org.thingsboard.server.common.data.ai.model.chat.AiChatModel; |
|||
import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; |
|||
import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; |
|||
|
|||
@Service |
|||
@RequiredArgsConstructor |
|||
class AiModelServiceImpl implements RuleEngineAiModelService { |
|||
|
|||
private final Langchain4jChatModelConfigurer chatModelConfigurer; |
|||
|
|||
@Override |
|||
public <C extends AiChatModelConfig<C>> ChatModel configureChatModel(AiChatModel<C> chatModel) { |
|||
return chatModel.configure(chatModelConfigurer); |
|||
} |
|||
|
|||
} |
|||
@ -1,106 +0,0 @@ |
|||
/** |
|||
* 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.chat.ChatModel; |
|||
import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; |
|||
import dev.langchain4j.model.mistralai.MistralAiChatModel; |
|||
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.ai.model.AiModelConfig; |
|||
import org.thingsboard.server.common.data.ai.model.GoogleAiGeminiChatModelConfig; |
|||
import org.thingsboard.server.common.data.ai.model.MistralAiChatModelConfig; |
|||
import org.thingsboard.server.common.data.ai.model.OpenAiChatModelConfig; |
|||
import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; |
|||
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.time.Duration; |
|||
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 configureChatModel(aiSettings.getProviderConfig(), aiSettings.getModelConfig()); |
|||
} |
|||
|
|||
@Override |
|||
public ChatModel configureChatModel(AiProviderConfig providerConfig, AiModelConfig modelConfig) { |
|||
return switch (providerConfig.getProvider()) { |
|||
case OPENAI -> { |
|||
var modelBuilder = OpenAiChatModel.builder() |
|||
.apiKey(providerConfig.getApiKey()) |
|||
.modelName(modelConfig.getModel()); |
|||
|
|||
if (modelConfig instanceof OpenAiChatModelConfig config) { |
|||
modelBuilder.temperature(config.getTemperature()); |
|||
if (config.getTimeoutSeconds() != null) { |
|||
modelBuilder.timeout(Duration.ofSeconds(config.getTimeoutSeconds())); |
|||
} |
|||
modelBuilder.maxRetries(config.getMaxRetries()); |
|||
} |
|||
|
|||
yield modelBuilder.build(); |
|||
} |
|||
case MISTRAL_AI -> { |
|||
var modelBuilder = MistralAiChatModel.builder() |
|||
.apiKey(providerConfig.getApiKey()) |
|||
.modelName(modelConfig.getModel()); |
|||
|
|||
if (modelConfig instanceof MistralAiChatModelConfig config) { |
|||
modelBuilder.temperature(config.getTemperature()); |
|||
if (config.getTimeoutSeconds() != null) { |
|||
modelBuilder.timeout(Duration.ofSeconds(config.getTimeoutSeconds())); |
|||
} |
|||
modelBuilder.maxRetries(config.getMaxRetries()); |
|||
} |
|||
|
|||
yield modelBuilder.build(); |
|||
} |
|||
case GOOGLE_AI_GEMINI -> { |
|||
var modelBuilder = GoogleAiGeminiChatModel.builder() |
|||
.apiKey(providerConfig.getApiKey()) |
|||
.modelName(modelConfig.getModel()); |
|||
|
|||
if (modelConfig instanceof GoogleAiGeminiChatModelConfig config) { |
|||
modelBuilder.temperature(config.getTemperature()); |
|||
if (config.getTimeoutSeconds() != null) { |
|||
modelBuilder.timeout(Duration.ofSeconds(config.getTimeoutSeconds())); |
|||
} |
|||
modelBuilder.maxRetries(config.getMaxRetries()); |
|||
} |
|||
|
|||
yield modelBuilder.build(); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
/** |
|||
* 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.chat.ChatModel; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModel; |
|||
import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; |
|||
import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModel; |
|||
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModel; |
|||
|
|||
import java.time.Duration; |
|||
|
|||
@Component |
|||
public class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer { |
|||
|
|||
@Override |
|||
public ChatModel configureChatModel(OpenAiChatModel chatModel) { |
|||
OpenAiChatModel.Config modelConfig = chatModel.modelConfig(); |
|||
return dev.langchain4j.model.openai.OpenAiChatModel.builder() |
|||
.apiKey(chatModel.providerConfig().apiKey()) |
|||
.modelName(chatModel.modelId()) |
|||
.temperature(modelConfig.temperature()) |
|||
.timeout(toDuration(modelConfig.timeoutSeconds())) |
|||
.maxRetries(modelConfig.maxRetries()) |
|||
.build(); |
|||
} |
|||
|
|||
@Override |
|||
public ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel) { |
|||
GoogleAiGeminiChatModel.Config modelConfig = chatModel.modelConfig(); |
|||
return dev.langchain4j.model.googleai.GoogleAiGeminiChatModel.builder() |
|||
.apiKey(chatModel.providerConfig().apiKey()) |
|||
.modelName(chatModel.modelId()) |
|||
.temperature(modelConfig.temperature()) |
|||
.timeout(toDuration(modelConfig.timeoutSeconds())) |
|||
.maxRetries(modelConfig.maxRetries()) |
|||
.build(); |
|||
} |
|||
|
|||
@Override |
|||
public ChatModel configureChatModel(MistralAiChatModel chatModel) { |
|||
MistralAiChatModel.Config modelConfig = chatModel.modelConfig(); |
|||
return dev.langchain4j.model.mistralai.MistralAiChatModel.builder() |
|||
.apiKey(chatModel.providerConfig().apiKey()) |
|||
.modelName(chatModel.modelId()) |
|||
.temperature(modelConfig.temperature()) |
|||
.timeout(toDuration(modelConfig.timeoutSeconds())) |
|||
.maxRetries(modelConfig.maxRetries()) |
|||
.build(); |
|||
} |
|||
|
|||
private static Duration toDuration(Integer timeoutSeconds) { |
|||
return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
/** |
|||
* 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.model; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonSubTypes; |
|||
import com.fasterxml.jackson.annotation.JsonTypeInfo; |
|||
import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModel; |
|||
import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModel; |
|||
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModel; |
|||
import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; |
|||
|
|||
@JsonTypeInfo( |
|||
use = JsonTypeInfo.Id.NAME, |
|||
include = JsonTypeInfo.As.PROPERTY, |
|||
property = "modelId", |
|||
visible = true |
|||
) |
|||
@JsonSubTypes({ |
|||
@JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4o"), |
|||
@JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.5-flash"), |
|||
@JsonSubTypes.Type(value = MistralAiChatModel.class, name = "mistral-medium-latest") |
|||
}) |
|||
public interface AiModel<C extends AiModelConfig<C>> { |
|||
|
|||
AiProviderConfig providerConfig(); |
|||
|
|||
AiModelType modelType(); |
|||
|
|||
String modelId(); |
|||
|
|||
C modelConfig(); |
|||
|
|||
AiModel<C> withModelConfig(C config); |
|||
|
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
/** |
|||
* 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.model; |
|||
|
|||
public enum AiModelType { |
|||
|
|||
CHAT |
|||
|
|||
} |
|||
@ -1,62 +0,0 @@ |
|||
/** |
|||
* 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.model; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@NoArgsConstructor |
|||
@Schema( |
|||
name = "GoogleAiGeminiChatModelConfig", |
|||
description = "Configuration for Google AI Gemini chat models" |
|||
) |
|||
public final class GoogleAiGeminiChatModelConfig extends AiModelConfig { |
|||
|
|||
@Schema( |
|||
requiredMode = Schema.RequiredMode.REQUIRED, |
|||
accessMode = Schema.AccessMode.READ_WRITE, |
|||
description = "Identifier of the AI model", |
|||
allowableValues = "gemini-2.0-flash", |
|||
example = "gemini-2.0-flash" |
|||
) |
|||
public String getModel() { |
|||
return super.getModel(); |
|||
} |
|||
|
|||
@Schema( |
|||
accessMode = Schema.AccessMode.READ_WRITE, |
|||
description = "Sampling temperature to control randomness: 0.0 (most deterministic) to 1.0 (most creative)", |
|||
example = "0.7" |
|||
) |
|||
private Double temperature; |
|||
|
|||
@Schema( |
|||
accessMode = Schema.AccessMode.READ_WRITE, |
|||
description = "Timeout (in seconds) for establishing HTTP connection" |
|||
) |
|||
private Integer timeoutSeconds; |
|||
|
|||
@Schema( |
|||
accessMode = Schema.AccessMode.READ_WRITE, |
|||
description = "Maximum number of times to retry an LLM call upon exception (except for non-retriable ones like authentication or invalid request errors)" |
|||
) |
|||
private Integer maxRetries; |
|||
|
|||
} |
|||
@ -1,62 +0,0 @@ |
|||
/** |
|||
* 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.model; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@NoArgsConstructor |
|||
@Schema( |
|||
name = "MistralAiChatModelConfig", |
|||
description = "Configuration for Mistral AI chat models" |
|||
) |
|||
public final class MistralAiChatModelConfig extends AiModelConfig { |
|||
|
|||
@Schema( |
|||
requiredMode = Schema.RequiredMode.REQUIRED, |
|||
accessMode = Schema.AccessMode.READ_WRITE, |
|||
description = "Identifier of the AI model", |
|||
allowableValues = "mistral-medium-latest", |
|||
example = "mistral-medium-latest" |
|||
) |
|||
public String getModel() { |
|||
return super.getModel(); |
|||
} |
|||
|
|||
@Schema( |
|||
accessMode = Schema.AccessMode.READ_WRITE, |
|||
description = "Sampling temperature to control randomness: 0.0 (most deterministic) to 1.0 (most creative)", |
|||
example = "0.7" |
|||
) |
|||
private Double temperature; |
|||
|
|||
@Schema( |
|||
accessMode = Schema.AccessMode.READ_WRITE, |
|||
description = "Timeout (in seconds) for the entire HTTP call: applied to connect, read, and write operations" |
|||
) |
|||
private Integer timeoutSeconds; |
|||
|
|||
@Schema( |
|||
accessMode = Schema.AccessMode.READ_WRITE, |
|||
description = "Maximum number of times to retry an LLM call upon exception (except for non-retriable ones like authentication or invalid request errors)" |
|||
) |
|||
private Integer maxRetries; |
|||
|
|||
} |
|||
@ -1,62 +0,0 @@ |
|||
/** |
|||
* 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.model; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@NoArgsConstructor |
|||
@Schema( |
|||
name = "OpenAiChatModelConfig", |
|||
description = "Configuration for OpenAI chat models" |
|||
) |
|||
public final class OpenAiChatModelConfig extends AiModelConfig { |
|||
|
|||
@Schema( |
|||
requiredMode = Schema.RequiredMode.REQUIRED, |
|||
accessMode = Schema.AccessMode.READ_WRITE, |
|||
description = "Identifier of the AI model", |
|||
allowableValues = {"gpt-4o", "gpt-4o-mini"}, |
|||
example = "gpt-4o" |
|||
) |
|||
public String getModel() { |
|||
return super.getModel(); |
|||
} |
|||
|
|||
@Schema( |
|||
accessMode = Schema.AccessMode.READ_WRITE, |
|||
description = "Sampling temperature to control randomness: 0.0 (most deterministic) to 1.0 (most creative)", |
|||
example = "0.7" |
|||
) |
|||
private Double temperature; |
|||
|
|||
@Schema( |
|||
accessMode = Schema.AccessMode.READ_WRITE, |
|||
description = "Timeout (in seconds) for both establishing HTTP connection and receiving a response" |
|||
) |
|||
private Integer timeoutSeconds; |
|||
|
|||
@Schema( |
|||
accessMode = Schema.AccessMode.READ_WRITE, |
|||
description = "Maximum number of times to retry an LLM call upon exception (except for non-retriable ones like authentication or invalid request errors)" |
|||
) |
|||
private Integer maxRetries; |
|||
|
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
/** |
|||
* 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.model.chat; |
|||
|
|||
import dev.langchain4j.model.chat.ChatModel; |
|||
import org.thingsboard.server.common.data.ai.model.AiModel; |
|||
import org.thingsboard.server.common.data.ai.model.AiModelType; |
|||
|
|||
public sealed interface AiChatModel<C extends AiChatModelConfig<C>> extends AiModel<C> |
|||
permits OpenAiChatModel, GoogleAiGeminiChatModel, MistralAiChatModel { |
|||
|
|||
ChatModel configure(Langchain4jChatModelConfigurer configurer); |
|||
|
|||
@Override |
|||
default AiModelType modelType() { |
|||
return AiModelType.CHAT; |
|||
} |
|||
|
|||
@Override |
|||
C modelConfig(); |
|||
|
|||
@Override |
|||
AiChatModel<C> withModelConfig(C config); |
|||
|
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
/** |
|||
* 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.model.chat; |
|||
|
|||
import org.thingsboard.server.common.data.ai.model.AiModelConfig; |
|||
|
|||
public sealed interface AiChatModelConfig<C extends AiChatModelConfig<C>> extends AiModelConfig<C> |
|||
permits OpenAiChatModel.Config, GoogleAiGeminiChatModel.Config, MistralAiChatModel.Config { |
|||
|
|||
Double temperature(); |
|||
|
|||
Integer timeoutSeconds(); |
|||
|
|||
Integer maxRetries(); |
|||
|
|||
C withTemperature(Double temperature); |
|||
|
|||
C withTimeoutSeconds(Integer timeoutSeconds); |
|||
|
|||
C withMaxRetries(Integer maxRetries); |
|||
|
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
/** |
|||
* 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.model.chat; |
|||
|
|||
import dev.langchain4j.model.chat.ChatModel; |
|||
import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; |
|||
|
|||
public record GoogleAiGeminiChatModel( |
|||
GoogleAiGeminiProviderConfig providerConfig, |
|||
String modelId, |
|||
Config modelConfig |
|||
) implements AiChatModel<GoogleAiGeminiChatModel.Config> { |
|||
|
|||
public record Config( |
|||
Double temperature, |
|||
Integer timeoutSeconds, |
|||
Integer maxRetries |
|||
) implements AiChatModelConfig<GoogleAiGeminiChatModel.Config> { |
|||
|
|||
@Override |
|||
public Config withTemperature(Double temperature) { |
|||
return new Config(temperature, timeoutSeconds, maxRetries); |
|||
} |
|||
|
|||
@Override |
|||
public Config withTimeoutSeconds(Integer timeoutSeconds) { |
|||
return new Config(temperature, timeoutSeconds, maxRetries); |
|||
} |
|||
|
|||
@Override |
|||
public Config withMaxRetries(Integer maxRetries) { |
|||
return new Config(temperature, timeoutSeconds, maxRetries); |
|||
} |
|||
|
|||
} |
|||
|
|||
@Override |
|||
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { |
|||
return configurer.configureChatModel(this); |
|||
} |
|||
|
|||
@Override |
|||
public GoogleAiGeminiChatModel withModelConfig(GoogleAiGeminiChatModel.Config config) { |
|||
return new GoogleAiGeminiChatModel(providerConfig, modelId, config); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.ai.model.chat; |
|||
|
|||
import dev.langchain4j.model.chat.ChatModel; |
|||
|
|||
public interface Langchain4jChatModelConfigurer { |
|||
|
|||
ChatModel configureChatModel(OpenAiChatModel chatModel); |
|||
|
|||
ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel); |
|||
|
|||
ChatModel configureChatModel(MistralAiChatModel chatModel); |
|||
|
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
/** |
|||
* 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.model.chat; |
|||
|
|||
import dev.langchain4j.model.chat.ChatModel; |
|||
import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; |
|||
|
|||
public record MistralAiChatModel( |
|||
MistralAiProviderConfig providerConfig, |
|||
String modelId, |
|||
Config modelConfig |
|||
) implements AiChatModel<MistralAiChatModel.Config> { |
|||
|
|||
public record Config( |
|||
Double temperature, |
|||
Integer timeoutSeconds, |
|||
Integer maxRetries |
|||
) implements AiChatModelConfig<MistralAiChatModel.Config> { |
|||
|
|||
@Override |
|||
public Config withTemperature(Double temperature) { |
|||
return new Config(temperature, timeoutSeconds, maxRetries); |
|||
} |
|||
|
|||
@Override |
|||
public Config withTimeoutSeconds(Integer timeoutSeconds) { |
|||
return new Config(temperature, timeoutSeconds, maxRetries); |
|||
} |
|||
|
|||
@Override |
|||
public Config withMaxRetries(Integer maxRetries) { |
|||
return new Config(temperature, timeoutSeconds, maxRetries); |
|||
} |
|||
|
|||
} |
|||
|
|||
@Override |
|||
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { |
|||
return configurer.configureChatModel(this); |
|||
} |
|||
|
|||
@Override |
|||
public MistralAiChatModel withModelConfig(Config config) { |
|||
return new MistralAiChatModel(providerConfig, modelId, config); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
/** |
|||
* 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.model.chat; |
|||
|
|||
import dev.langchain4j.model.chat.ChatModel; |
|||
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; |
|||
|
|||
public record OpenAiChatModel( |
|||
OpenAiProviderConfig providerConfig, |
|||
String modelId, |
|||
Config modelConfig |
|||
) implements AiChatModel<OpenAiChatModel.Config> { |
|||
|
|||
public record Config( |
|||
Double temperature, |
|||
Integer timeoutSeconds, |
|||
Integer maxRetries |
|||
) implements AiChatModelConfig<OpenAiChatModel.Config> { |
|||
|
|||
@Override |
|||
public OpenAiChatModel.Config withTemperature(Double temperature) { |
|||
return new Config(temperature, timeoutSeconds, maxRetries); |
|||
} |
|||
|
|||
@Override |
|||
public OpenAiChatModel.Config withTimeoutSeconds(Integer timeoutSeconds) { |
|||
return new Config(temperature, timeoutSeconds, maxRetries); |
|||
} |
|||
|
|||
@Override |
|||
public OpenAiChatModel.Config withMaxRetries(Integer maxRetries) { |
|||
return new Config(temperature, timeoutSeconds, maxRetries); |
|||
} |
|||
|
|||
} |
|||
|
|||
@Override |
|||
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { |
|||
return configurer.configureChatModel(this); |
|||
} |
|||
|
|||
@Override |
|||
public OpenAiChatModel withModelConfig(OpenAiChatModel.Config config) { |
|||
return new OpenAiChatModel(providerConfig, modelId, config); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
/** |
|||
* 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.service.validator; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.ai.AiModelSettings; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.dao.ai.AiModelSettingsDao; |
|||
import org.thingsboard.server.dao.exception.DataValidationException; |
|||
import org.thingsboard.server.dao.service.DataValidator; |
|||
import org.thingsboard.server.dao.tenant.TenantService; |
|||
|
|||
import java.util.Optional; |
|||
|
|||
@Component |
|||
@RequiredArgsConstructor |
|||
class AiModelSettingsDataValidator extends DataValidator<AiModelSettings> { |
|||
|
|||
private final TenantService tenantService; |
|||
private final AiModelSettingsDao aiModelSettingsDao; |
|||
|
|||
@Override |
|||
protected void validateCreate(TenantId tenantId, AiModelSettings settings) { |
|||
validateNumberOfEntitiesPerTenant(tenantId, EntityType.AI_MODEL_SETTINGS); |
|||
} |
|||
|
|||
@Override |
|||
protected AiModelSettings validateUpdate(TenantId tenantId, AiModelSettings settings) { |
|||
Optional<AiModelSettings> existing = aiModelSettingsDao.findByTenantIdAndId(tenantId, settings.getId()); |
|||
if (existing.isEmpty()) { |
|||
throw new DataValidationException("Cannot update non-existent AI model settings!"); |
|||
} |
|||
return existing.get(); |
|||
} |
|||
|
|||
@Override |
|||
protected void validateDataImpl(TenantId tenantId, AiModelSettings settings) { |
|||
// ID validation
|
|||
if (settings.getId() != null) { |
|||
if (settings.getUuidId() == null) { |
|||
throw new DataValidationException("AI model settings UUID should be specified!"); |
|||
} |
|||
if (settings.getId().isNullUid()) { |
|||
throw new DataValidationException("AI model settings UUID must not be the reserved null value!"); |
|||
} |
|||
} |
|||
|
|||
// tenant ID validation
|
|||
if (settings.getTenantId() == null || settings.getTenantId().getId() == null) { |
|||
throw new DataValidationException("AI model settings should be assigned to tenant!"); |
|||
} |
|||
if (settings.getTenantId().isSysTenantId()) { |
|||
throw new DataValidationException("AI model settings cannot be assigned to the system tenant!"); |
|||
} |
|||
if (!tenantService.tenantExists(tenantId)) { |
|||
throw new DataValidationException("AI model settings reference a non-existent tenant!"); |
|||
} |
|||
|
|||
// name validation
|
|||
validateString("AI model settings name", settings.getName()); |
|||
if (settings.getName().length() > 255) { |
|||
throw new DataValidationException("AI model settings name should be between 1 and 255 symbols!"); |
|||
} |
|||
|
|||
// model config validation
|
|||
if (settings.getConfiguration() == null) { |
|||
throw new DataValidationException("AI model settings configuration should be specified!"); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -1,109 +0,0 @@ |
|||
/** |
|||
* 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.service.validator; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
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.id.TenantId; |
|||
import org.thingsboard.server.dao.ai.AiSettingsDao; |
|||
import org.thingsboard.server.dao.exception.DataValidationException; |
|||
import org.thingsboard.server.dao.service.DataValidator; |
|||
import org.thingsboard.server.dao.tenant.TenantService; |
|||
|
|||
import java.util.Objects; |
|||
import java.util.Optional; |
|||
|
|||
@Component |
|||
@RequiredArgsConstructor |
|||
class AiSettingsDataValidator extends DataValidator<AiSettings> { |
|||
|
|||
private final TenantService tenantService; |
|||
private final AiSettingsDao aiSettingsDao; |
|||
|
|||
@Override |
|||
protected void validateCreate(TenantId tenantId, AiSettings aiSettings) { |
|||
validateNumberOfEntitiesPerTenant(tenantId, EntityType.AI_SETTINGS); |
|||
} |
|||
|
|||
@Override |
|||
protected AiSettings validateUpdate(TenantId tenantId, AiSettings aiSettings) { |
|||
Optional<AiSettings> old = aiSettingsDao.findByTenantIdAndId(tenantId, aiSettings.getId()); |
|||
if (old.isEmpty()) { |
|||
throw new DataValidationException("Cannot update non-existent AI settings!"); |
|||
} |
|||
return old.get(); |
|||
} |
|||
|
|||
@Override |
|||
protected void validateDataImpl(TenantId tenantId, AiSettings aiSettings) { |
|||
// ID validation
|
|||
if (aiSettings.getId() != null) { |
|||
if (aiSettings.getUuidId() == null) { |
|||
throw new DataValidationException("AI settings UUID should be specified!"); |
|||
} |
|||
if (aiSettings.getId().isNullUid()) { |
|||
throw new DataValidationException("AI settings UUID must not be the reserved null value!"); |
|||
} |
|||
} |
|||
|
|||
// tenant ID validation
|
|||
if (aiSettings.getTenantId() == null || aiSettings.getTenantId().getId() == null) { |
|||
throw new DataValidationException("AI settings should be assigned to tenant!"); |
|||
} |
|||
if (aiSettings.getTenantId().isSysTenantId()) { |
|||
throw new DataValidationException("AI settings cannot be assigned to the system tenant!"); |
|||
} |
|||
if (!tenantService.tenantExists(tenantId)) { |
|||
throw new DataValidationException("AI settings reference a non-existent tenant!"); |
|||
} |
|||
|
|||
// name validation
|
|||
validateString("AI settings name", aiSettings.getName()); |
|||
if (aiSettings.getName().length() > 255) { |
|||
throw new DataValidationException("AI settings name should be between 1 and 255 symbols!"); |
|||
} |
|||
|
|||
// provider validation
|
|||
if (aiSettings.getProvider() == null) { |
|||
throw new DataValidationException("AI provider should be specified!"); |
|||
} |
|||
|
|||
// provider config validation
|
|||
if (aiSettings.getProviderConfig() == null) { |
|||
throw new DataValidationException("AI provider config should be specified!"); |
|||
} |
|||
if (aiSettings.getProviderConfig().getProvider() != aiSettings.getProvider()) { |
|||
throw new DataValidationException("AI provider configuration should match the selected AI provider!"); |
|||
} |
|||
validateString("AI provider API key", aiSettings.getProviderConfig().getApiKey()); |
|||
|
|||
// model identifier validation
|
|||
validateString("AI model identifier", aiSettings.getModel()); |
|||
if (aiSettings.getModel().length() > 255) { |
|||
throw new DataValidationException("AI model identifier should be between 1 and 255 symbols!"); |
|||
} |
|||
|
|||
// model config validation
|
|||
if (aiSettings.getModelConfig() != null) { |
|||
if (!Objects.equals(aiSettings.getModelConfig().getModel(), aiSettings.getModel())) { |
|||
throw new DataValidationException("AI model configuration should match the selected AI model!"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue