14 changed files with 368 additions and 44 deletions
@ -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 {} |
|||
@ -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 |
|||
) {} |
|||
Loading…
Reference in new issue