diff --git a/application/pom.xml b/application/pom.xml index 7ec326758b..e92aeba533 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -383,11 +383,11 @@ dev.langchain4j - langchain4j-azure-open-ai + langchain4j-open-ai dev.langchain4j - langchain4j-open-ai + langchain4j-azure-open-ai dev.langchain4j @@ -401,6 +401,10 @@ dev.langchain4j langchain4j-mistral-ai + + dev.langchain4j + langchain4j-anthropic + 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 91e9c7874e..aa4aed988d 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 @@ -25,6 +25,7 @@ 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.AnthropicChatModel; 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; @@ -125,6 +126,18 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .build(); } + @Override + public ChatModel configureChatModel(AnthropicChatModel chatModel) { + AnthropicChatModel.Config modelConfig = chatModel.modelConfig(); + return dev.langchain4j.model.anthropic.AnthropicChatModel.builder() + .apiKey(chatModel.providerConfig().apiKey()) + .modelName(modelConfig.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; } 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 index 34dab4b6aa..0f3634e1f4 100644 --- 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 @@ -19,6 +19,7 @@ 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.AnthropicChatModel; 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; @@ -77,6 +78,14 @@ public final class AiModelTypeIdResolver extends TypeIdResolverBase { map.put("MISTRAL_AI::ministral-3b-latest", MistralAiChatModel.class); map.put("MISTRAL_AI::open-mistral-nemo", MistralAiChatModel.class); + // Anthropic models + map.put("ANTHROPIC::claude-opus-4-0", AnthropicChatModel.class); + map.put("ANTHROPIC::claude-sonnet-4-0", AnthropicChatModel.class); + map.put("ANTHROPIC::claude-3-7-sonnet-latest", AnthropicChatModel.class); + map.put("ANTHROPIC::claude-3-5-sonnet-latest", AnthropicChatModel.class); + map.put("ANTHROPIC::claude-3-5-haiku-latest", AnthropicChatModel.class); + map.put("ANTHROPIC::claude-3-opus-latest", AnthropicChatModel.class); + typeIdToModelClass = Collections.unmodifiableMap(map); } 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 08d4630d10..2043efd860 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,9 @@ 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, GoogleVertexAiGeminiChatModel, MistralAiChatModel { + permits + OpenAiChatModel, AzureOpenAiChatModel, GoogleAiGeminiChatModel, + GoogleVertexAiGeminiChatModel, MistralAiChatModel, AnthropicChatModel { 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 4ef5369005..d346b22731 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,9 @@ 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, GoogleVertexAiGeminiChatModel.Config, MistralAiChatModel.Config { + permits + OpenAiChatModel.Config, AzureOpenAiChatModel.Config, GoogleAiGeminiChatModel.Config, + GoogleVertexAiGeminiChatModel.Config, MistralAiChatModel.Config, AnthropicChatModel.Config { Double temperature(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java new file mode 100644 index 0000000000..329f35382a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.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.AnthropicProviderConfig; + +public record AnthropicChatModel( + AnthropicProviderConfig providerConfig, + Config modelConfig +) implements AiChatModel { + + public record Config( + String modelId, + Double temperature, + Integer timeoutSeconds, + Integer maxRetries + ) implements AiChatModelConfig { + + @Override + public AnthropicChatModel.Config withTemperature(Double temperature) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + @Override + public AnthropicChatModel.Config withTimeoutSeconds(Integer timeoutSeconds) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + @Override + public AnthropicChatModel.Config withMaxRetries(Integer maxRetries) { + return new Config(modelId, temperature, timeoutSeconds, maxRetries); + } + + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public AnthropicChatModel withModelConfig(AnthropicChatModel.Config config) { + return new AnthropicChatModel(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 3602fb14a7..3cb75419f0 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 @@ -29,4 +29,6 @@ public interface Langchain4jChatModelConfigurer { ChatModel configureChatModel(MistralAiChatModel chatModel); + ChatModel configureChatModel(AnthropicChatModel chatModel); + } 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 da4df1a076..78709fd52c 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 @@ -21,6 +21,7 @@ public enum AiProvider { AZURE_OPENAI, GOOGLE_AI_GEMINI, GOOGLE_VERTEX_AI_GEMINI, - MISTRAL_AI + MISTRAL_AI, + ANTHROPIC } 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 e3b3b250cc..e13f3fac7b 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 @@ -28,10 +28,13 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @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") + @JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI"), + @JsonSubTypes.Type(value = AnthropicProviderConfig.class, name = "ANTHROPIC") }) public sealed interface AiProviderConfig - permits OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig, GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig { + permits + OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig, + GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig, AnthropicProviderConfig { AiProvider provider(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.java new file mode 100644 index 0000000000..91ef41a072 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.java @@ -0,0 +1,25 @@ +/** + * 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; + +public record AnthropicProviderConfig(String apiKey) implements AiProviderConfig { + + @Override + public AiProvider provider() { + return AiProvider.ANTHROPIC; + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 2ac3ea3e6c..edcf2065eb 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -16,7 +16,6 @@ package org.thingsboard.rule.engine.ai; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.FutureCallback; import dev.langchain4j.data.message.SystemMessage; @@ -25,7 +24,6 @@ import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.request.ResponseFormat; import dev.langchain4j.model.chat.request.ResponseFormatType; -import dev.langchain4j.model.chat.request.json.JsonSchema; import dev.langchain4j.model.chat.response.ChatResponse; import org.checkerframework.checker.nullness.qual.NonNull; import org.thingsboard.common.util.JacksonUtil; @@ -81,10 +79,13 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { throw new TbNodeException(e, true); } - responseFormat = ResponseFormat.builder() - .type(config.getResponseFormatType()) - .jsonSchema(getJsonSchema(config.getResponseFormatType(), config.getJsonSchema())) - .build(); + // LC4j AnthropicChatModel rejects requests with non-null ResponseFormat even if ResponseFormatType is TEXT + if (config.getResponseFormatType() == ResponseFormatType.JSON) { + responseFormat = ResponseFormat.builder() + .type(config.getResponseFormatType()) + .jsonSchema(config.getJsonSchema() != null ? Langchain4jJsonSchemaAdapter.fromJsonNode(config.getJsonSchema()) : null) + .build(); + } systemPrompt = config.getSystemPrompt(); userPrompt = config.getUserPrompt(); @@ -101,13 +102,6 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { } } - private static JsonSchema getJsonSchema(ResponseFormatType responseFormatType, ObjectNode jsonSchema) { - if (responseFormatType == ResponseFormatType.TEXT) { - return null; - } - return responseFormatType == ResponseFormatType.JSON && jsonSchema != null ? Langchain4jJsonSchemaAdapter.fromJsonNode(jsonSchema) : null; - } - @Override public void onMsg(TbContext ctx, TbMsg msg) { var ackedMsg = ackIfNeeded(ctx, msg);