AI rule node: add API for sending arbitrary chat requests to LLM model
This commit is contained in:
		
							parent
							
								
									944d80df9d
								
							
						
					
					
						commit
						a2e7ff293b
					
				@ -30,7 +30,6 @@ import org.springframework.context.annotation.Lazy;
 | 
			
		||||
import org.springframework.data.redis.core.RedisTemplate;
 | 
			
		||||
import org.springframework.stereotype.Component;
 | 
			
		||||
import org.thingsboard.common.util.JacksonUtil;
 | 
			
		||||
import org.thingsboard.rule.engine.api.AiRequestsExecutor;
 | 
			
		||||
import org.thingsboard.rule.engine.api.DeviceStateManager;
 | 
			
		||||
import org.thingsboard.rule.engine.api.JobManager;
 | 
			
		||||
import org.thingsboard.rule.engine.api.MailService;
 | 
			
		||||
@ -322,10 +321,6 @@ public class ActorSystemContext {
 | 
			
		||||
    @Getter
 | 
			
		||||
    private AiModelSettingsService aiModelSettingsService;
 | 
			
		||||
 | 
			
		||||
    @Autowired
 | 
			
		||||
    @Getter
 | 
			
		||||
    private AiRequestsExecutor aiRequestsExecutor;
 | 
			
		||||
 | 
			
		||||
    @Autowired
 | 
			
		||||
    @Getter
 | 
			
		||||
    private EntityViewService entityViewService;
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,6 @@ import org.bouncycastle.util.Arrays;
 | 
			
		||||
import org.thingsboard.common.util.DebugModeUtil;
 | 
			
		||||
import org.thingsboard.common.util.JacksonUtil;
 | 
			
		||||
import org.thingsboard.common.util.ListeningExecutor;
 | 
			
		||||
import org.thingsboard.rule.engine.api.AiRequestsExecutor;
 | 
			
		||||
import org.thingsboard.rule.engine.api.DeviceStateManager;
 | 
			
		||||
import org.thingsboard.rule.engine.api.JobManager;
 | 
			
		||||
import org.thingsboard.rule.engine.api.MailService;
 | 
			
		||||
@ -1037,11 +1036,6 @@ public class DefaultTbContext implements TbContext {
 | 
			
		||||
        return mainCtx.getAiModelSettingsService();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public AiRequestsExecutor getAiRequestsExecutor() {
 | 
			
		||||
        return mainCtx.getAiRequestsExecutor();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MqttClientSettings getMqttClientSettings() {
 | 
			
		||||
        return mainCtx.getMqttClientSettings();
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,63 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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 com.google.common.util.concurrent.ListenableFuture;
 | 
			
		||||
import dev.langchain4j.model.chat.request.ChatRequest;
 | 
			
		||||
import jakarta.validation.Valid;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import org.springframework.security.access.prepost.PreAuthorize;
 | 
			
		||||
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.springframework.web.context.request.async.DeferredResult;
 | 
			
		||||
import org.thingsboard.server.common.data.ai.dto.TbChatRequest;
 | 
			
		||||
import org.thingsboard.server.common.data.ai.dto.TbChatResponse;
 | 
			
		||||
import org.thingsboard.server.common.data.ai.model.chat.AiChatModel;
 | 
			
		||||
import org.thingsboard.server.config.annotations.ApiOperation;
 | 
			
		||||
import org.thingsboard.server.service.ai.AiModelService;
 | 
			
		||||
 | 
			
		||||
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
 | 
			
		||||
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
 | 
			
		||||
 | 
			
		||||
@RestController
 | 
			
		||||
@RequiredArgsConstructor
 | 
			
		||||
@RequestMapping("/api/ai/model")
 | 
			
		||||
class AiModelController extends BaseController {
 | 
			
		||||
 | 
			
		||||
    private final AiModelService aiModelService;
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(
 | 
			
		||||
            value = "Send request to AI chat model (sendChatRequest)",
 | 
			
		||||
            notes = "Submits a single prompt - made up of an optional system message and a required user message - to the specified AI chat model " +
 | 
			
		||||
                    "and returns either the generated answer or an error envelope." +
 | 
			
		||||
                    TENANT_AUTHORITY_PARAGRAPH
 | 
			
		||||
    )
 | 
			
		||||
    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
 | 
			
		||||
    @PostMapping("/chat")
 | 
			
		||||
    public DeferredResult<TbChatResponse> sendChatRequest(@Valid @RequestBody TbChatRequest tbChatRequest) {
 | 
			
		||||
        ChatRequest langChainChatRequest = tbChatRequest.toLangChainChatRequest();
 | 
			
		||||
        AiChatModel<?> chatModel = tbChatRequest.chatModel();
 | 
			
		||||
 | 
			
		||||
        ListenableFuture<TbChatResponse> future = aiModelService.sendChatRequestAsync(chatModel, langChainChatRequest)
 | 
			
		||||
                .transform(chatResponse -> (TbChatResponse) new TbChatResponse.Success(chatResponse.aiMessage().text()), directExecutor())
 | 
			
		||||
                .catching(Throwable.class, ex -> new TbChatResponse.Failure(ex.getMessage()), directExecutor());
 | 
			
		||||
 | 
			
		||||
        return wrapFuture(future);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,20 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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 org.thingsboard.rule.engine.api.RuleEngineAiModelService;
 | 
			
		||||
 | 
			
		||||
public interface AiModelService extends RuleEngineAiModelService {}
 | 
			
		||||
@ -15,23 +15,27 @@
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.service.ai;
 | 
			
		||||
 | 
			
		||||
import com.google.common.util.concurrent.FluentFuture;
 | 
			
		||||
import dev.langchain4j.model.chat.ChatModel;
 | 
			
		||||
import dev.langchain4j.model.chat.request.ChatRequest;
 | 
			
		||||
import dev.langchain4j.model.chat.response.ChatResponse;
 | 
			
		||||
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 {
 | 
			
		||||
class AiModelServiceImpl implements AiModelService {
 | 
			
		||||
 | 
			
		||||
    private final Langchain4jChatModelConfigurer chatModelConfigurer;
 | 
			
		||||
    private final AiRequestsExecutor aiRequestsExecutor;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public <C extends AiChatModelConfig<C>> ChatModel configureChatModel(AiChatModel<C> chatModel) {
 | 
			
		||||
        return chatModel.configure(chatModelConfigurer);
 | 
			
		||||
    public <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModel<C> chatModel, ChatRequest chatRequest) {
 | 
			
		||||
        ChatModel lc4jChatModel = chatModel.configure(chatModelConfigurer);
 | 
			
		||||
        return aiRequestsExecutor.sendChatRequestAsync(lc4jChatModel, chatRequest);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.rule.engine.api;
 | 
			
		||||
package org.thingsboard.server.service.ai;
 | 
			
		||||
 | 
			
		||||
import com.google.common.util.concurrent.FluentFuture;
 | 
			
		||||
import dev.langchain4j.model.chat.ChatModel;
 | 
			
		||||
@ -33,7 +33,6 @@ import org.springframework.context.annotation.Lazy;
 | 
			
		||||
import org.springframework.stereotype.Component;
 | 
			
		||||
import org.springframework.validation.annotation.Validated;
 | 
			
		||||
import org.thingsboard.common.util.ThingsBoardThreadFactory;
 | 
			
		||||
import org.thingsboard.rule.engine.api.AiRequestsExecutor;
 | 
			
		||||
 | 
			
		||||
import java.time.Duration;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,79 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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.dto;
 | 
			
		||||
 | 
			
		||||
import dev.langchain4j.data.message.ChatMessage;
 | 
			
		||||
import dev.langchain4j.data.message.Content;
 | 
			
		||||
import dev.langchain4j.data.message.SystemMessage;
 | 
			
		||||
import dev.langchain4j.data.message.UserMessage;
 | 
			
		||||
import dev.langchain4j.model.chat.request.ChatRequest;
 | 
			
		||||
import io.swagger.v3.oas.annotations.media.Schema;
 | 
			
		||||
import jakarta.validation.Valid;
 | 
			
		||||
import jakarta.validation.constraints.NotNull;
 | 
			
		||||
import org.thingsboard.server.common.data.ai.model.chat.AiChatModel;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
public record TbChatRequest(
 | 
			
		||||
        @Schema(
 | 
			
		||||
                requiredMode = Schema.RequiredMode.NOT_REQUIRED,
 | 
			
		||||
                accessMode = Schema.AccessMode.READ_WRITE,
 | 
			
		||||
                description = "A system-level instruction that frames the user's input, setting the persona, tone, and constraints for the generated response",
 | 
			
		||||
                example = "You are a helpful assistant. Only output valid JSON."
 | 
			
		||||
        )
 | 
			
		||||
        String systemMessage,
 | 
			
		||||
 | 
			
		||||
        @Schema(
 | 
			
		||||
                requiredMode = Schema.RequiredMode.REQUIRED,
 | 
			
		||||
                accessMode = Schema.AccessMode.READ_WRITE,
 | 
			
		||||
                description = "The actual user prompt that will be answered by the AI model"
 | 
			
		||||
        )
 | 
			
		||||
        @NotNull @Valid
 | 
			
		||||
        TbUserMessage userMessage,
 | 
			
		||||
 | 
			
		||||
        @Schema(
 | 
			
		||||
                requiredMode = Schema.RequiredMode.REQUIRED,
 | 
			
		||||
                accessMode = Schema.AccessMode.READ_WRITE,
 | 
			
		||||
                description = "Configuration of the AI chat model that should execute the request"
 | 
			
		||||
        )
 | 
			
		||||
        @NotNull @Valid
 | 
			
		||||
        AiChatModel<?> chatModel
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
    public ChatRequest toLangChainChatRequest() {
 | 
			
		||||
        return ChatRequest.builder()
 | 
			
		||||
                .messages(getLangChainMessages())
 | 
			
		||||
                .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private List<ChatMessage> getLangChainMessages() {
 | 
			
		||||
        List<ChatMessage> messages = new ArrayList<>(2);
 | 
			
		||||
 | 
			
		||||
        if (systemMessage != null) {
 | 
			
		||||
            messages.add(SystemMessage.from(systemMessage));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        List<Content> langChainContents = userMessage.contents().stream()
 | 
			
		||||
                .map(TbContent::toLangChainContent)
 | 
			
		||||
                .toList();
 | 
			
		||||
 | 
			
		||||
        messages.add(UserMessage.from(langChainContents));
 | 
			
		||||
 | 
			
		||||
        return messages;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,68 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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.dto;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
 | 
			
		||||
import io.swagger.v3.oas.annotations.media.Schema;
 | 
			
		||||
 | 
			
		||||
@JsonTypeInfo(
 | 
			
		||||
        use = JsonTypeInfo.Id.NAME,
 | 
			
		||||
        property = "status",
 | 
			
		||||
        include = JsonTypeInfo.As.PROPERTY,
 | 
			
		||||
        visible = true
 | 
			
		||||
)
 | 
			
		||||
@JsonSubTypes({
 | 
			
		||||
        @JsonSubTypes.Type(value = TbChatResponse.Success.class, name = "SUCCESS"),
 | 
			
		||||
        @JsonSubTypes.Type(value = TbChatResponse.Failure.class, name = "FAILURE")
 | 
			
		||||
})
 | 
			
		||||
public sealed interface TbChatResponse permits TbChatResponse.Success, TbChatResponse.Failure {
 | 
			
		||||
 | 
			
		||||
    @Schema(
 | 
			
		||||
            description = "Indicates whether the request was successful or not",
 | 
			
		||||
            example = "SUCCESS"
 | 
			
		||||
    )
 | 
			
		||||
    String getStatus();
 | 
			
		||||
 | 
			
		||||
    record Success(
 | 
			
		||||
            @Schema(description = "The text content generated by the model")
 | 
			
		||||
            String generatedContent
 | 
			
		||||
    ) implements TbChatResponse {
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        @Schema(example = "SUCCESS")
 | 
			
		||||
        public String getStatus() {
 | 
			
		||||
            return "SUCCESS";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    record Failure(
 | 
			
		||||
            @Schema(
 | 
			
		||||
                    description = "A string containing details about the failure"
 | 
			
		||||
            )
 | 
			
		||||
            String errorDetails
 | 
			
		||||
    ) implements TbChatResponse {
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        @Schema(example = "FAILURE")
 | 
			
		||||
        public String getStatus() {
 | 
			
		||||
            return "FAILURE";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,73 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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.dto;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
 | 
			
		||||
import dev.langchain4j.data.message.Content;
 | 
			
		||||
import dev.langchain4j.data.message.TextContent;
 | 
			
		||||
import io.swagger.v3.oas.annotations.media.Schema;
 | 
			
		||||
import jakarta.validation.constraints.NotBlank;
 | 
			
		||||
 | 
			
		||||
import static org.thingsboard.server.common.data.ai.dto.TbContent.TbTextContent;
 | 
			
		||||
 | 
			
		||||
@JsonTypeInfo(
 | 
			
		||||
        use = JsonTypeInfo.Id.NAME,
 | 
			
		||||
        include = JsonTypeInfo.As.PROPERTY,
 | 
			
		||||
        property = "contentType",
 | 
			
		||||
        visible = true
 | 
			
		||||
)
 | 
			
		||||
@JsonSubTypes({
 | 
			
		||||
        @JsonSubTypes.Type(value = TbTextContent.class, name = "TEXT")
 | 
			
		||||
})
 | 
			
		||||
public sealed interface TbContent permits TbTextContent {
 | 
			
		||||
 | 
			
		||||
    TbContentType contentType();
 | 
			
		||||
 | 
			
		||||
    Content toLangChainContent();
 | 
			
		||||
 | 
			
		||||
    enum TbContentType {
 | 
			
		||||
 | 
			
		||||
        TEXT
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Schema(
 | 
			
		||||
            description = "Text-based content part of a user's prompt"
 | 
			
		||||
    )
 | 
			
		||||
    record TbTextContent(
 | 
			
		||||
            @NotBlank
 | 
			
		||||
            @Schema(
 | 
			
		||||
                    requiredMode = Schema.RequiredMode.REQUIRED,
 | 
			
		||||
                    description = "The text content",
 | 
			
		||||
                    example = "What is the weather like in Kyiv today?"
 | 
			
		||||
            )
 | 
			
		||||
            String text
 | 
			
		||||
    ) implements TbContent {
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public TbContentType contentType() {
 | 
			
		||||
            return TbContentType.TEXT;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public Content toLangChainContent() {
 | 
			
		||||
            return TextContent.from(text);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,32 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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.dto;
 | 
			
		||||
 | 
			
		||||
import io.swagger.v3.oas.annotations.media.Schema;
 | 
			
		||||
import jakarta.validation.Valid;
 | 
			
		||||
import jakarta.validation.constraints.NotEmpty;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
public record TbUserMessage(
 | 
			
		||||
        @NotEmpty
 | 
			
		||||
        @Valid
 | 
			
		||||
        @Schema(
 | 
			
		||||
                requiredMode = Schema.RequiredMode.REQUIRED,
 | 
			
		||||
                description = "A list of content parts that make up the complete user prompt"
 | 
			
		||||
        )
 | 
			
		||||
        List<TbContent> contents
 | 
			
		||||
) {}
 | 
			
		||||
@ -15,12 +15,14 @@
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.rule.engine.api;
 | 
			
		||||
 | 
			
		||||
import dev.langchain4j.model.chat.ChatModel;
 | 
			
		||||
import com.google.common.util.concurrent.FluentFuture;
 | 
			
		||||
import dev.langchain4j.model.chat.request.ChatRequest;
 | 
			
		||||
import dev.langchain4j.model.chat.response.ChatResponse;
 | 
			
		||||
import org.thingsboard.server.common.data.ai.model.chat.AiChatModel;
 | 
			
		||||
import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig;
 | 
			
		||||
 | 
			
		||||
public interface RuleEngineAiModelService {
 | 
			
		||||
 | 
			
		||||
    <C extends AiChatModelConfig<C>> ChatModel configureChatModel(AiChatModel<C> chatModel);
 | 
			
		||||
    <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModel<C> chatModel, ChatRequest chatRequest);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -427,8 +427,6 @@ public interface TbContext {
 | 
			
		||||
 | 
			
		||||
    AiModelSettingsService getAiModelSettingsService();
 | 
			
		||||
 | 
			
		||||
    AiRequestsExecutor getAiRequestsExecutor();
 | 
			
		||||
 | 
			
		||||
    // Configuration parameters for the MQTT client that is used in the MQTT node and Azure IoT hub node
 | 
			
		||||
 | 
			
		||||
    MqttClientSettings getMqttClientSettings();
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,6 @@ import com.google.common.util.concurrent.FutureCallback;
 | 
			
		||||
import dev.langchain4j.data.message.ChatMessage;
 | 
			
		||||
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.chat.request.ResponseFormatType;
 | 
			
		||||
@ -119,29 +118,27 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
 | 
			
		||||
                .responseFormat(responseFormat)
 | 
			
		||||
                .build();
 | 
			
		||||
 | 
			
		||||
        configureChatModelAsync(ctx)
 | 
			
		||||
                .transformAsync(chatModel -> ctx.getAiRequestsExecutor().sendChatRequestAsync(chatModel, chatRequest), directExecutor())
 | 
			
		||||
                .addCallback(new FutureCallback<>() {
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void onSuccess(ChatResponse chatResponse) {
 | 
			
		||||
                        String response = chatResponse.aiMessage().text();
 | 
			
		||||
                        if (!isValidJsonObject(response)) {
 | 
			
		||||
                            response = wrapInJsonObject(response);
 | 
			
		||||
                        }
 | 
			
		||||
                        tellSuccess(ctx, ackedMsg.transform()
 | 
			
		||||
                                .data(response)
 | 
			
		||||
                                .build());
 | 
			
		||||
                    }
 | 
			
		||||
        sendChatRequestAsync(ctx, chatRequest).addCallback(new FutureCallback<>() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onSuccess(ChatResponse chatResponse) {
 | 
			
		||||
                String response = chatResponse.aiMessage().text();
 | 
			
		||||
                if (!isValidJsonObject(response)) {
 | 
			
		||||
                    response = wrapInJsonObject(response);
 | 
			
		||||
                }
 | 
			
		||||
                tellSuccess(ctx, ackedMsg.transform()
 | 
			
		||||
                        .data(response)
 | 
			
		||||
                        .build());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void onFailure(@NonNull Throwable t) {
 | 
			
		||||
                        tellFailure(ctx, ackedMsg, t);
 | 
			
		||||
                    }
 | 
			
		||||
                }, directExecutor());
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onFailure(@NonNull Throwable t) {
 | 
			
		||||
                tellFailure(ctx, ackedMsg, t);
 | 
			
		||||
            }
 | 
			
		||||
        }, directExecutor());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private <C extends AiChatModelConfig<C>> FluentFuture<ChatModel> configureChatModelAsync(TbContext ctx) {
 | 
			
		||||
        return ctx.getAiModelSettingsService().findAiModelSettingsByTenantIdAndIdAsync(ctx.getTenantId(), modelSettingsId).transform(settingsOpt -> {
 | 
			
		||||
    private <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(TbContext ctx, ChatRequest chatRequest) {
 | 
			
		||||
        return ctx.getAiModelSettingsService().findAiModelSettingsByTenantIdAndIdAsync(ctx.getTenantId(), modelSettingsId).transformAsync(settingsOpt -> {
 | 
			
		||||
            if (settingsOpt.isEmpty()) {
 | 
			
		||||
                throw new NoSuchElementException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] were not found");
 | 
			
		||||
            }
 | 
			
		||||
@ -158,7 +155,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
 | 
			
		||||
                    .withTimeoutSeconds(timeoutSeconds)
 | 
			
		||||
                    .withMaxRetries(0)); // disable retries to respect timeout set in rule node config
 | 
			
		||||
 | 
			
		||||
            return ctx.getAiModelService().configureChatModel(chatModel);
 | 
			
		||||
            return ctx.getAiModelService().sendChatRequestAsync(chatModel, chatRequest);
 | 
			
		||||
        }, ctx.getDbCallbackExecutor());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user