From 1d2f5eab2fd0d7463a40494fe9817d74e331d25f Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Fri, 10 Apr 2026 06:04:02 -0600 Subject: [PATCH] Add OpenRouter bot --- .../controller/ChatWebSocketController.java | 14 ++- .../petshop/backend/service/ChatService.java | 22 +++++ .../backend/service/OpenRouterAiService.java | 90 +++++++++++++++++++ backend/src/main/resources/application.yml | 5 +- .../resources/db/migration/V2__seed_data.sql | 3 +- .../dev/final-target/final_target_seed.sql | 3 +- 6 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 backend/src/main/java/com/petshop/backend/service/OpenRouterAiService.java diff --git a/backend/src/main/java/com/petshop/backend/controller/ChatWebSocketController.java b/backend/src/main/java/com/petshop/backend/controller/ChatWebSocketController.java index ed0a3718..5df48490 100644 --- a/backend/src/main/java/com/petshop/backend/controller/ChatWebSocketController.java +++ b/backend/src/main/java/com/petshop/backend/controller/ChatWebSocketController.java @@ -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}) diff --git a/backend/src/main/java/com/petshop/backend/service/ChatService.java b/backend/src/main/java/com/petshop/backend/service/ChatService.java index e17f550c..9cc00823 100644 --- a/backend/src/main/java/com/petshop/backend/service/ChatService.java +++ b/backend/src/main/java/com/petshop/backend/service/ChatService.java @@ -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) diff --git a/backend/src/main/java/com/petshop/backend/service/OpenRouterAiService.java b/backend/src/main/java/com/petshop/backend/service/OpenRouterAiService.java new file mode 100644 index 00000000..e9e622cf --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/OpenRouterAiService.java @@ -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 history = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); + + List> 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 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 response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200) { + Map responseMap = objectMapper.readValue(response.body(), Map.class); + List> choices = (List>) responseMap.get("choices"); + if (choices != null && !choices.isEmpty()) { + Map choice = choices.get(0); + Map message = (Map) 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(); + } + }); + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 1c9a800e..644bd23f 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -70,4 +70,7 @@ logging: serialization: write-dates-as-timestamps: false deserialization: - fail-on-unknown-properties: false \ No newline at end of file + fail-on-unknown-properties: false +openrouter: + api-key: ${OPENROUTER_API_KEY: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} + model: ${OPENROUTER_MODEL:mistralai/mistral-7b-instruct:free} diff --git a/backend/src/main/resources/db/migration/V2__seed_data.sql b/backend/src/main/resources/db/migration/V2__seed_data.sql index aab02cc1..4d4503c2 100644 --- a/backend/src/main/resources/db/migration/V2__seed_data.sql +++ b/backend/src/main/resources/db/migration/V2__seed_data.sql @@ -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'), diff --git a/backend/src/main/resources/dev/final-target/final_target_seed.sql b/backend/src/main/resources/dev/final-target/final_target_seed.sql index f1db3380..447f1dad 100644 --- a/backend/src/main/resources/dev/final-target/final_target_seed.sql +++ b/backend/src/main/resources/dev/final-target/final_target_seed.sql @@ -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'),