Add OpenRouter bot

This commit is contained in:
2026-04-10 06:04:02 -06:00
parent 50150b22b7
commit 1d2f5eab2f
6 changed files with 133 additions and 4 deletions

View File

@@ -9,6 +9,8 @@ import com.petshop.backend.security.AppPrincipal;
import com.petshop.backend.security.JwtUtil;
import com.petshop.backend.service.ChatRealtimeService;
import com.petshop.backend.service.ChatService;
import com.petshop.backend.service.OpenRouterAiService;
import com.petshop.backend.entity.Conversation;
import jakarta.validation.Valid;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
@@ -27,6 +29,7 @@ import java.util.Map;
public class ChatWebSocketController {
private final ChatService chatService;
private final OpenRouterAiService openRouterAiService;
private final ChatRealtimeService chatRealtimeService;
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
@@ -37,8 +40,10 @@ public class ChatWebSocketController {
ChatRealtimeService chatRealtimeService,
UserRepository userRepository,
JwtUtil jwtUtil,
WebSocketAuthChannelInterceptor webSocketAuthChannelInterceptor
WebSocketAuthChannelInterceptor webSocketAuthChannelInterceptor,
OpenRouterAiService openRouterAiService
) {
this.openRouterAiService = openRouterAiService;
this.chatService = chatService;
this.chatRealtimeService = chatRealtimeService;
this.userRepository = userRepository;
@@ -53,6 +58,13 @@ public class ChatWebSocketController {
MessageResponse message = chatService.sendMessage(id, user.getId(), user.getRole(), request);
chatRealtimeService.publishMessage(id, message);
chatRealtimeService.publishConversationUpdate(id);
if (user.getRole() == User.Role.CUSTOMER) {
Conversation conversation = chatService.getConversationEntity(id);
if (conversation.getMode() == Conversation.ConversationMode.AUTOMATED) {
openRouterAiService.generateAndSendReply(id);
}
}
}
@MessageExceptionHandler({IllegalArgumentException.class, RuntimeException.class})

View File

@@ -89,6 +89,12 @@ public class ChatService {
.collect(Collectors.toList());
}
public Conversation getConversationEntity(Long id) {
return conversationRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
}
public ConversationResponse getConversation(Long conversationId, Long userId, User.Role role) {
Conversation conversation = conversationRepository.findById(conversationId)
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
@@ -151,6 +157,22 @@ public class ChatService {
return toMessageResponse(message);
}
@Transactional
public MessageResponse saveBotMessage(Long conversationId, String content) {
Conversation conversation = conversationRepository.findById(conversationId)
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
Message message = new Message();
message.setConversationId(conversationId);
message.setSenderId(101L);
message.setContent(content);
message.setIsRead(false);
message = messageRepository.save(message);
return toMessageResponse(message);
}
@Transactional
public MessageResponse sendMessageWithAttachment(Long conversationId, Long userId, User.Role role, MultipartFile file, String content) {
Conversation conversation = conversationRepository.findById(conversationId)

View File

@@ -0,0 +1,90 @@
package com.petshop.backend.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.petshop.backend.entity.Message;
import com.petshop.backend.repository.MessageRepository;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Value;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@Service
public class OpenRouterAiService {
@Value("${openrouter.api-key}")
private String apiKey;
@Value("${openrouter.model}")
private String model;
private final String openRouterUrl = "https://openrouter.ai/api/v1/chat/completions";
private final ChatService chatService;
private final ChatRealtimeService chatRealtimeService;
private final MessageRepository messageRepository;
private final ObjectMapper objectMapper;
private final HttpClient httpClient;
public OpenRouterAiService(ChatService chatService, ChatRealtimeService chatRealtimeService, MessageRepository messageRepository, ObjectMapper objectMapper) {
this.chatService = chatService;
this.chatRealtimeService = chatRealtimeService;
this.messageRepository = messageRepository;
this.objectMapper = objectMapper;
this.httpClient = HttpClient.newHttpClient();
}
public void generateAndSendReply(Long conversationId) {
CompletableFuture.runAsync(() -> {
try {
List<Message> history = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
List<Map<String, String>> messages = history.stream().map(msg -> {
String role = (msg.getSenderId() != null && msg.getSenderId() == 101L) ? "assistant" : "user";
return Map.of("role", role, "content", msg.getContent() != null ? msg.getContent() : "");
}).collect(Collectors.toList());
messages.add(0, Map.of("role", "system", "content", "You are a helpful pet shop assistant. Provide concise and friendly answers. Do not output markdown, just plain text."));
Map<String, Object> requestBody = Map.of(
"model", model,
"messages", messages
);
String requestJson = objectMapper.writeValueAsString(requestBody);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(openRouterUrl))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
.POST(HttpRequest.BodyPublishers.ofString(requestJson))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
Map<String, Object> responseMap = objectMapper.readValue(response.body(), Map.class);
List<Map<String, Object>> choices = (List<Map<String, Object>>) responseMap.get("choices");
if (choices != null && !choices.isEmpty()) {
Map<String, Object> choice = choices.get(0);
Map<String, String> message = (Map<String, String>) choice.get("message");
String replyContent = message.get("content");
var messageResponse = chatService.saveBotMessage(conversationId, replyContent);
chatRealtimeService.publishMessage(conversationId, messageResponse);
chatRealtimeService.publishConversationUpdate(conversationId);
}
} else {
System.err.println("OpenRouter API Error: " + response.statusCode() + " " + response.body());
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
}

View File

@@ -70,4 +70,7 @@ logging:
serialization:
write-dates-as-timestamps: false
deserialization:
fail-on-unknown-properties: false
fail-on-unknown-properties: false
openrouter:
api-key: ${OPENROUTER_API_KEY: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}
model: ${OPENROUTER_MODEL:mistralai/mistral-7b-instruct:free}

View File

@@ -155,7 +155,8 @@ INSERT INTO users (id, username, password, email, firstName, lastName, fullName,
(97, 'alex.murray', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.murray@gmail.com', 'Alex', 'Murray', 'Alex Murray', '403-730-0097', 'https://images.petshop.local/users/097.webp', 'CUSTOMER', 'CUSTOMER', NULL, 4, 1, 0),
(98, 'alex.freeman', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.freeman@gmail.com', 'Alex', 'Freeman', 'Alex Freeman', '403-730-0098', 'https://images.petshop.local/users/098.webp', 'CUSTOMER', 'CUSTOMER', NULL, 0, 1, 0),
(99, 'alex.wells', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.wells@gmail.com', 'Alex', 'Wells', 'Alex Wells', '403-730-0099', 'https://images.petshop.local/users/099.webp', 'CUSTOMER', 'CUSTOMER', NULL, 0, 1, 0),
(100, 'alex.webb', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.webb@gmail.com', 'Alex', 'Webb', 'Alex Webb', '403-730-0100', 'https://images.petshop.local/users/100.webp', 'CUSTOMER', 'CUSTOMER', NULL, 0, 1, 0);
(100, 'alex.webb', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.webb@gmail.com', 'Alex', 'Webb', 'Alex Webb', '403-730-0100', 'https://images.petshop.local/users/100.webp', 'CUSTOMER', 'CUSTOMER', NULL, 0, 1, 0),
(101, 'ai.bot', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'bot@petshop.com', 'AI', 'Bot', 'AI Bot', '000-000-0000', 'https://images.petshop.local/users/bot.webp', 'STAFF', 'CUSTOMER_SERVICE', NULL, 0, 1, 0);
INSERT INTO supplier (supId, supCompany, supContactFirstName, supContactLastName, supEmail, supPhone) VALUES
(1, 'PetFood Inc', 'Robert', 'King', 'contact@petfood.com', '403-601-1001'),

View File

@@ -153,7 +153,8 @@ INSERT INTO users (id, username, password, email, firstName, lastName, fullName,
(97, 'alex.murray', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.murray@gmail.com', 'Alex', 'Murray', 'Alex Murray', '403-730-0097', 'https://images.petshop.local/users/097.webp', 'CUSTOMER', 'CUSTOMER', 2, 4, 1, 0),
(98, 'alex.freeman', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.freeman@gmail.com', 'Alex', 'Freeman', 'Alex Freeman', '403-730-0098', 'https://images.petshop.local/users/098.webp', 'CUSTOMER', 'CUSTOMER', 3, 0, 1, 0),
(99, 'alex.wells', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.wells@gmail.com', 'Alex', 'Wells', 'Alex Wells', '403-730-0099', 'https://images.petshop.local/users/099.webp', 'CUSTOMER', 'CUSTOMER', 1, 0, 1, 0),
(100, 'alex.webb', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.webb@gmail.com', 'Alex', 'Webb', 'Alex Webb', '403-730-0100', 'https://images.petshop.local/users/100.webp', 'CUSTOMER', 'CUSTOMER', 2, 0, 1, 0);
(100, 'alex.webb', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.webb@gmail.com', 'Alex', 'Webb', 'Alex Webb', '403-730-0100', 'https://images.petshop.local/users/100.webp', 'CUSTOMER', 'CUSTOMER', 2, 0, 1, 0),
(101, 'ai.bot', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'bot@petshop.com', 'AI', 'Bot', 'AI Bot', '000-000-0000', 'https://images.petshop.local/users/bot.webp', 'STAFF', 'CUSTOMER_SERVICE', NULL, 0, 1, 0);
INSERT INTO supplier (supId, supCompany, supContactFirstName, supContactLastName, supEmail, supPhone) VALUES
(1, 'PetFood Inc', 'Robert', 'King', 'contact@petfood.com', '403-601-1001'),