diff --git a/application/pom.xml b/application/pom.xml index 2c11c50d3d..7ec326758b 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -393,6 +393,10 @@ dev.langchain4j langchain4j-google-ai-gemini + + dev.langchain4j + langchain4j-vertex-ai-gemini + dev.langchain4j langchain4j-mistral-ai diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index d3b23b65b5..91e9c7874e 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -15,25 +15,37 @@ */ package org.thingsboard.server.service.ai; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.cloud.vertexai.Transport; +import com.google.cloud.vertexai.VertexAI; +import com.google.cloud.vertexai.api.GenerationConfig; +import com.google.cloud.vertexai.generativeai.GenerativeModel; import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModel; import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModel; 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 org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.time.Duration; @Component -public class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer { +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()) + .modelName(modelConfig.modelId()) .temperature(modelConfig.temperature()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) @@ -45,7 +57,7 @@ public class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelC AzureOpenAiChatModel.Config modelConfig = chatModel.modelConfig(); return dev.langchain4j.model.azure.AzureOpenAiChatModel.builder() .apiKey(chatModel.providerConfig().apiKey()) - .deploymentName(chatModel.modelId()) + .deploymentName(modelConfig.modelId()) .temperature(modelConfig.temperature()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) @@ -57,19 +69,56 @@ public class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelC GoogleAiGeminiChatModel.Config modelConfig = chatModel.modelConfig(); return dev.langchain4j.model.googleai.GoogleAiGeminiChatModel.builder() .apiKey(chatModel.providerConfig().apiKey()) - .modelName(chatModel.modelId()) + .modelName(modelConfig.modelId()) .temperature(modelConfig.temperature()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) .build(); } + @Override + public ChatModel configureChatModel(GoogleVertexAiGeminiChatModel chatModel) { + GoogleVertexAiGeminiProviderConfig providerConfig = chatModel.providerConfig(); + GoogleVertexAiGeminiChatModel.Config modelConfig = chatModel.modelConfig(); + + // construct service account credentials using service account key JSON + ObjectNode serviceAccountKeyJson = providerConfig.serviceAccountKey(); + ServiceAccountCredentials serviceAccountCredentials; + try { + serviceAccountCredentials = ServiceAccountCredentials + .fromStream(new ByteArrayInputStream(JacksonUtil.writeValueAsBytes(serviceAccountKeyJson))); + } catch (IOException e) { + throw new RuntimeException("Failed to parse service account key JSON", e); + } + + // construct Vertex AI instance + var vertexAI = new VertexAI.Builder() + .setProjectId(providerConfig.projectId()) + .setLocation(providerConfig.location()) + .setCredentials(serviceAccountCredentials) + .setTransport(Transport.REST) // GRPC also possible, but likely does not work with service account keys + .build(); + + // map model config to generation config + var generationConfigBuilder = GenerationConfig.newBuilder(); + if (modelConfig.temperature() != null) { + generationConfigBuilder.setTemperature(modelConfig.temperature().floatValue()); + } + var generationConfig = generationConfigBuilder.build(); + + // construct generative model instance + var generativeModel = new GenerativeModel(modelConfig.modelId(), vertexAI) + .withGenerationConfig(generationConfig); + + return new VertexAiGeminiChatModel(generativeModel, generationConfig, modelConfig.maxRetries()); + } + @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()) + .modelName(modelConfig.modelId()) .temperature(modelConfig.temperature()) .timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(modelConfig.maxRetries()) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java index 8f88b7aa01..114fe2a528 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java @@ -15,61 +15,22 @@ */ 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 com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, + use = JsonTypeInfo.Id.CUSTOM, include = JsonTypeInfo.As.PROPERTY, - property = "modelId", - visible = true + property = "@type" ) -@JsonSubTypes({ - // OpenAI models - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o4-mini"), - // @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o3-pro"), needs verification with Gov ID :) - // @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o3"), needs verification with Gov ID :) - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o3-mini"), - // @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o1-pro"), LC4j sends requests to v1/chat/completions, but o1-pro is only supported in v1/responses - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "o1"), - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4.1"), - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4.1-mini"), - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4.1-nano"), - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4o"), - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4o-mini"), - - // Google AI Gemini models - @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.5-pro"), - @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.5-flash"), - @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.0-flash"), - @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.0-flash-lite"), - @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-1.5-pro"), - @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-1.5-flash"), - @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-1.5-flash-8b"), - - // Mistral AI models - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "magistral-medium-latest"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "magistral-small-latest"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "mistral-large-latest"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "mistral-medium-latest"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "mistral-small-latest"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "pixtral-large-latest"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "ministral-8b-latest"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "ministral-3b-latest"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "open-mistral-nemo") -}) +@JsonTypeIdResolver(AiModelTypeIdResolver.class) public interface AiModel> { AiProviderConfig providerConfig(); AiModelType modelType(); - String modelId(); - C modelConfig(); AiModel withModelConfig(C config); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java index b07007c301..7f9526a4db 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java @@ -15,4 +15,8 @@ */ package org.thingsboard.server.common.data.ai.model; -public interface AiModelConfig> {} +public interface AiModelConfig> { + + String modelId(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java new file mode 100644 index 0000000000..34dab4b6aa --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelTypeIdResolver.java @@ -0,0 +1,120 @@ +/** + * 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.JsonTypeInfo; +import com.fasterxml.jackson.databind.DatabindContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase; +import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModel; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public final class AiModelTypeIdResolver extends TypeIdResolverBase { + + private static final Map> typeIdToModelClass; + + static { + Map> map = new HashMap<>(); + + // OpenAI models + map.put("OPENAI::o4-mini", OpenAiChatModel.class); + // map.put("OPENAI::o3-pro", OpenAiChatModel.class); // needs verification with Gov ID :) + // map.put("OPENAI::o3", OpenAiChatModel.class); // needs verification with Gov ID :) + map.put("OPENAI::o3-mini", OpenAiChatModel.class); + // map.put("OPENAI::o1-pro", OpenAiChatModel.class); // LC4j sends requests to v1/chat/completions, but o1-pro is only supported in v1/responses + map.put("OPENAI::o1", OpenAiChatModel.class); + map.put("OPENAI::gpt-4.1", OpenAiChatModel.class); + map.put("OPENAI::gpt-4.1-mini", OpenAiChatModel.class); + map.put("OPENAI::gpt-4.1-nano", OpenAiChatModel.class); + map.put("OPENAI::gpt-4o", OpenAiChatModel.class); + map.put("OPENAI::gpt-4o-mini", OpenAiChatModel.class); + + // Google AI Gemini models + map.put("GOOGLE_AI_GEMINI::gemini-2.5-pro", GoogleAiGeminiChatModel.class); + map.put("GOOGLE_AI_GEMINI::gemini-2.5-flash", GoogleAiGeminiChatModel.class); + map.put("GOOGLE_AI_GEMINI::gemini-2.0-flash", GoogleAiGeminiChatModel.class); + map.put("GOOGLE_AI_GEMINI::gemini-2.0-flash-lite", GoogleAiGeminiChatModel.class); + map.put("GOOGLE_AI_GEMINI::gemini-1.5-pro", GoogleAiGeminiChatModel.class); + map.put("GOOGLE_AI_GEMINI::gemini-1.5-flash", GoogleAiGeminiChatModel.class); + map.put("GOOGLE_AI_GEMINI::gemini-1.5-flash-8b", GoogleAiGeminiChatModel.class); + + // Google Vertex AI Gemini models + map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-2.5-pro", GoogleVertexAiGeminiChatModel.class); + map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-2.5-flash", GoogleVertexAiGeminiChatModel.class); + map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-2.0-flash", GoogleVertexAiGeminiChatModel.class); + map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-2.0-flash-lite", GoogleVertexAiGeminiChatModel.class); + map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-1.5-pro", GoogleVertexAiGeminiChatModel.class); + map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-1.5-flash", GoogleVertexAiGeminiChatModel.class); + map.put("GOOGLE_VERTEX_AI_GEMINI::gemini-1.5-flash-8b", GoogleVertexAiGeminiChatModel.class); + + // Mistral AI models + map.put("MISTRAL_AI::magistral-medium-latest", MistralAiChatModel.class); + map.put("MISTRAL_AI::magistral-small-latest", MistralAiChatModel.class); + map.put("MISTRAL_AI::mistral-large-latest", MistralAiChatModel.class); + map.put("MISTRAL_AI::mistral-medium-latest", MistralAiChatModel.class); + map.put("MISTRAL_AI::mistral-small-latest", MistralAiChatModel.class); + map.put("MISTRAL_AI::pixtral-large-latest", MistralAiChatModel.class); + map.put("MISTRAL_AI::ministral-8b-latest", MistralAiChatModel.class); + map.put("MISTRAL_AI::ministral-3b-latest", MistralAiChatModel.class); + map.put("MISTRAL_AI::open-mistral-nemo", MistralAiChatModel.class); + + typeIdToModelClass = Collections.unmodifiableMap(map); + } + + private JavaType baseType; + + @Override + public void init(JavaType baseType) { + this.baseType = baseType; + } + + @Override + public String idFromValue(Object value) { + return generateId((AiModel) value); + } + + @Override + public String idFromValueAndType(Object value, Class suggestedType) { + return generateId((AiModel) value); + } + + @Override + public JavaType typeFromId(DatabindContext context, String id) { + Class modelClass = typeIdToModelClass.get(id); + if (modelClass == null) { + throw new IllegalArgumentException("Unknown model type ID: " + id); + } + return context.constructSpecializedType(baseType, modelClass); + } + + @Override + public JsonTypeInfo.Id getMechanism() { + return JsonTypeInfo.Id.CUSTOM; + } + + private static String generateId(AiModel model) { + String provider = model.providerConfig().provider().name(); + String modelId = model.modelConfig().modelId(); + return provider + "::" + modelId; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java index 4e944ed6ec..08d4630d10 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java @@ -20,7 +20,7 @@ import org.thingsboard.server.common.data.ai.model.AiModel; import org.thingsboard.server.common.data.ai.model.AiModelType; public sealed interface AiChatModel> extends AiModel - permits OpenAiChatModel, AzureOpenAiChatModel, GoogleAiGeminiChatModel, MistralAiChatModel { + permits OpenAiChatModel, AzureOpenAiChatModel, GoogleAiGeminiChatModel, GoogleVertexAiGeminiChatModel, MistralAiChatModel { ChatModel configure(Langchain4jChatModelConfigurer configurer); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java index e44c9c5fa1..4ef5369005 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java @@ -18,7 +18,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import org.thingsboard.server.common.data.ai.model.AiModelConfig; public sealed interface AiChatModelConfig> extends AiModelConfig - permits OpenAiChatModel.Config, AzureOpenAiChatModel.Config, GoogleAiGeminiChatModel.Config, MistralAiChatModel.Config { + permits OpenAiChatModel.Config, AzureOpenAiChatModel.Config, GoogleAiGeminiChatModel.Config, GoogleVertexAiGeminiChatModel.Config, MistralAiChatModel.Config { Double temperature(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java index 37a1d84095..9920b546f0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java @@ -20,11 +20,11 @@ import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; public record AzureOpenAiChatModel( AzureOpenAiProviderConfig providerConfig, - String modelId, Config modelConfig ) implements AiChatModel { public record Config( + String modelId, Double temperature, Integer timeoutSeconds, Integer maxRetries @@ -32,17 +32,17 @@ public record AzureOpenAiChatModel( @Override public AzureOpenAiChatModel.Config withTemperature(Double temperature) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } @Override public AzureOpenAiChatModel.Config withTimeoutSeconds(Integer timeoutSeconds) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } @Override public AzureOpenAiChatModel.Config withMaxRetries(Integer maxRetries) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } } @@ -54,7 +54,7 @@ public record AzureOpenAiChatModel( @Override public AzureOpenAiChatModel withModelConfig(AzureOpenAiChatModel.Config config) { - return new AzureOpenAiChatModel(providerConfig, modelId, config); + return new AzureOpenAiChatModel(providerConfig, config); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java index 875262abb6..c09903b305 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java @@ -20,11 +20,11 @@ import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConf public record GoogleAiGeminiChatModel( GoogleAiGeminiProviderConfig providerConfig, - String modelId, Config modelConfig ) implements AiChatModel { public record Config( + String modelId, Double temperature, Integer timeoutSeconds, Integer maxRetries @@ -32,17 +32,17 @@ public record GoogleAiGeminiChatModel( @Override public Config withTemperature(Double temperature) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } @Override public Config withTimeoutSeconds(Integer timeoutSeconds) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } @Override public Config withMaxRetries(Integer maxRetries) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } } @@ -54,7 +54,7 @@ public record GoogleAiGeminiChatModel( @Override public GoogleAiGeminiChatModel withModelConfig(GoogleAiGeminiChatModel.Config config) { - return new GoogleAiGeminiChatModel(providerConfig, modelId, config); + return new GoogleAiGeminiChatModel(providerConfig, config); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java new file mode 100644 index 0000000000..a340430828 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java @@ -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.GoogleVertexAiGeminiProviderConfig; + +public record GoogleVertexAiGeminiChatModel( + GoogleVertexAiGeminiProviderConfig providerConfig, + Config modelConfig +) implements AiChatModel { + + public record Config( + String modelId, + Double temperature, + Integer timeoutSeconds, // TODO: not supported by Vertex AI + Integer maxRetries + ) implements AiChatModelConfig { + + @Override + public Config withTemperature(Double temperature) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + @Override + public Config withTimeoutSeconds(Integer timeoutSeconds) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + @Override + public Config withMaxRetries(Integer maxRetries) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public GoogleVertexAiGeminiChatModel withModelConfig(GoogleVertexAiGeminiChatModel.Config config) { + return new GoogleVertexAiGeminiChatModel(providerConfig, config); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java index 41dd1fe4ad..3602fb14a7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java @@ -25,6 +25,8 @@ public interface Langchain4jChatModelConfigurer { ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel); + ChatModel configureChatModel(GoogleVertexAiGeminiChatModel chatModel); + ChatModel configureChatModel(MistralAiChatModel chatModel); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java index 413d2b93a8..e4eae1b766 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java @@ -20,11 +20,11 @@ import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; public record MistralAiChatModel( MistralAiProviderConfig providerConfig, - String modelId, Config modelConfig ) implements AiChatModel { public record Config( + String modelId, Double temperature, Integer timeoutSeconds, Integer maxRetries @@ -32,17 +32,17 @@ public record MistralAiChatModel( @Override public Config withTemperature(Double temperature) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } @Override public Config withTimeoutSeconds(Integer timeoutSeconds) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } @Override public Config withMaxRetries(Integer maxRetries) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } } @@ -54,7 +54,7 @@ public record MistralAiChatModel( @Override public MistralAiChatModel withModelConfig(Config config) { - return new MistralAiChatModel(providerConfig, modelId, config); + return new MistralAiChatModel(providerConfig, config); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java index 0d5031d512..a4d7401cbf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java @@ -20,11 +20,11 @@ import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; public record OpenAiChatModel( OpenAiProviderConfig providerConfig, - String modelId, Config modelConfig ) implements AiChatModel { public record Config( + String modelId, Double temperature, Integer timeoutSeconds, Integer maxRetries @@ -32,17 +32,17 @@ public record OpenAiChatModel( @Override public OpenAiChatModel.Config withTemperature(Double temperature) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } @Override public OpenAiChatModel.Config withTimeoutSeconds(Integer timeoutSeconds) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } @Override public OpenAiChatModel.Config withMaxRetries(Integer maxRetries) { - return new Config(temperature, timeoutSeconds, maxRetries); + return new Config(modelId, temperature, timeoutSeconds, maxRetries); } } @@ -54,7 +54,7 @@ public record OpenAiChatModel( @Override public OpenAiChatModel withModelConfig(OpenAiChatModel.Config config) { - return new OpenAiChatModel(providerConfig, modelId, config); + return new OpenAiChatModel(providerConfig, config); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java index 20fb379f7d..da4df1a076 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java @@ -20,6 +20,7 @@ public enum AiProvider { OPENAI, AZURE_OPENAI, GOOGLE_AI_GEMINI, + GOOGLE_VERTEX_AI_GEMINI, MISTRAL_AI } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java index 84cda054d0..e3b3b250cc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java @@ -27,13 +27,12 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = OpenAiProviderConfig.class, name = "OPENAI"), @JsonSubTypes.Type(value = AzureOpenAiProviderConfig.class, name = "AZURE_OPENAI"), @JsonSubTypes.Type(value = GoogleAiGeminiProviderConfig.class, name = "GOOGLE_AI_GEMINI"), + @JsonSubTypes.Type(value = GoogleVertexAiGeminiProviderConfig.class, name = "GOOGLE_VERTEX_AI_GEMINI"), @JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI") }) public sealed interface AiProviderConfig - permits OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig, MistralAiProviderConfig { + permits OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig, GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig { AiProvider provider(); - String apiKey(); - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java index f9a2a98a21..0b948eabab 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java @@ -22,9 +22,4 @@ public record AzureOpenAiProviderConfig(String apiKey) implements AiProviderConf return AiProvider.AZURE_OPENAI; } - @Override - public String apiKey() { - return apiKey; - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java index 0bb9d21b52..35def6f0f5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java @@ -22,9 +22,4 @@ public record GoogleAiGeminiProviderConfig(String apiKey) implements AiProviderC return AiProvider.GOOGLE_AI_GEMINI; } - @Override - public String apiKey() { - return apiKey; - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java new file mode 100644 index 0000000000..a5140279ac --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.provider; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +public record GoogleVertexAiGeminiProviderConfig( + String projectId, + String location, + ObjectNode serviceAccountKey +) implements AiProviderConfig { + + @Override + public AiProvider provider() { + return AiProvider.GOOGLE_VERTEX_AI_GEMINI; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java index 45e3b68800..29c251b3cd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java @@ -22,9 +22,4 @@ public record MistralAiProviderConfig(String apiKey) implements AiProviderConfig return AiProvider.MISTRAL_AI; } - @Override - public String apiKey() { - return apiKey; - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java index 0536d1176e..6c069276d5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java @@ -22,9 +22,4 @@ public record OpenAiProviderConfig(String apiKey) implements AiProviderConfig { return AiProvider.OPENAI; } - @Override - public String apiKey() { - return apiKey; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java index e5909c5818..a86d5ba5c8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java @@ -35,15 +35,27 @@ interface AiModelSettingsRepository extends JpaRepository findByTenantIdAndName(UUID tenantId, String name); - @Query(nativeQuery = true, value = """ - SELECT * - FROM ai_model_settings ai_model - WHERE ai_model.tenant_id = :tenantId - AND (:textSearch IS NULL - OR ai_model.name ILIKE '%' || :textSearch || '%' - OR (ai_model.configuration -> 'providerConfig' ->> 'provider') ILIKE '%' || :textSearch || '%' - OR (ai_model.configuration ->> 'modelId') ILIKE '%' || :textSearch || '%') - """) + @Query( + value = """ + SELECT * + FROM ai_model_settings ai_model + WHERE ai_model.tenant_id = :tenantId + AND (:textSearch IS NULL + OR ai_model.name ILIKE '%' || :textSearch || '%' + OR (ai_model.configuration -> 'providerConfig' ->> 'provider') ILIKE '%' || :textSearch || '%' + OR (ai_model.configuration -> 'modelConfig' ->> 'modelId') ILIKE '%' || :textSearch || '%') + """, + countQuery = """ + SELECT COUNT(*) + FROM ai_model_settings ai_model + WHERE ai_model.tenant_id = :tenantId + AND (:textSearch IS NULL + OR ai_model.name ILIKE '%' || :textSearch || '%' + OR (ai_model.configuration -> 'providerConfig' ->> 'provider') ILIKE '%' || :textSearch || '%' + OR (ai_model.configuration -> 'modelConfig' ->> 'modelId') ILIKE '%' || :textSearch || '%') + """, + nativeQuery = true + ) Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT ai_model.id FROM AiModelSettingsEntity ai_model WHERE ai_model.tenantId = :tenantId")