From 0f5742caf4463bb12fc8139ed27f642f52e278ba Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Mon, 29 Dec 2025 10:31:15 -0800 Subject: [PATCH 01/11] Created controller and dummy service functions for Chatbot REST endpoint --- .../web/controllers/ChatbotController.java | 104 ++++++++++++ .../service/chatbot/ChatbotService.java | 63 +++++++ .../chatbot/impl/ChatbotServiceImpl.java | 159 ++++++++++++++++++ 3 files changed, 326 insertions(+) create mode 100644 src/main/java/org/wise/portal/presentation/web/controllers/ChatbotController.java create mode 100644 src/main/java/org/wise/portal/service/chatbot/ChatbotService.java create mode 100644 src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/ChatbotController.java b/src/main/java/org/wise/portal/presentation/web/controllers/ChatbotController.java new file mode 100644 index 000000000..dc13a41df --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/controllers/ChatbotController.java @@ -0,0 +1,104 @@ +package org.wise.portal.presentation.web.controllers; + +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.wise.portal.service.chatbot.ChatbotService; + +/** + * REST controller for managing chatbot conversations + * + * @author Hiroki Terashima + */ +@RestController +@RequestMapping("/api/chatbot") +@Secured("ROLE_USER") +public class ChatbotController { + + @Autowired + private ChatbotService chatbotService; + + /** + * Get all chats for a specific run and workgroup + * + * @param runId the run ID + * @param workgroupId the workgroup ID + * @return list of all chats + */ + @GetMapping("/chats/{runId}/{workgroupId}") + public ResponseEntity>> getAllChats(@PathVariable Long runId, + @PathVariable Long workgroupId) { + return ResponseEntity.ok(chatbotService.getAllChats(runId, workgroupId)); + } + + /** + * Get a specific chat by ID + * + * @param runId the run ID + * @param workgroupId the workgroup ID + * @param chatId the chat ID + * @return the requested chat + */ + @GetMapping("/chats/{runId}/{workgroupId}/{chatId}") + public ResponseEntity> getChat(@PathVariable Long runId, + @PathVariable Long workgroupId, @PathVariable String chatId) { + return ResponseEntity.ok(chatbotService.getChat(runId, workgroupId, chatId)); + } + + /** + * Create a new chat + * + * @param runId the run ID + * @param workgroupId the workgroup ID + * @param chatData the chat data + * @return the created chat with generated ID + */ + @PostMapping("/chats/{runId}/{workgroupId}") + public ResponseEntity> createChat(@PathVariable Long runId, + @PathVariable Long workgroupId, @RequestBody Map chatData) { + return ResponseEntity.status(HttpStatus.CREATED) + .body(chatbotService.createChat(runId, workgroupId, chatData)); + } + + /** + * Update an existing chat + * + * @param runId the run ID + * @param workgroupId the workgroup ID + * @param chatId the chat ID + * @param chatData the updated chat data + * @return the updated chat + */ + @PutMapping("/chats/{runId}/{workgroupId}/{chatId}") + public ResponseEntity> updateChat(@PathVariable Long runId, + @PathVariable Long workgroupId, @PathVariable String chatId, + @RequestBody Map chatData) { + return ResponseEntity.ok(chatbotService.updateChat(runId, workgroupId, chatId, chatData)); + } + + /** + * Delete a chat + * + * @param runId the run ID + * @param workgroupId the workgroup ID + * @param chatId the chat ID + * @return success response + */ + @DeleteMapping("/chats/{runId}/{workgroupId}/{chatId}") + public ResponseEntity> deleteChat(@PathVariable Long runId, + @PathVariable Long workgroupId, @PathVariable String chatId) { + return ResponseEntity.ok(chatbotService.deleteChat(runId, workgroupId, chatId)); + } +} diff --git a/src/main/java/org/wise/portal/service/chatbot/ChatbotService.java b/src/main/java/org/wise/portal/service/chatbot/ChatbotService.java new file mode 100644 index 000000000..c6e5b2c5a --- /dev/null +++ b/src/main/java/org/wise/portal/service/chatbot/ChatbotService.java @@ -0,0 +1,63 @@ +package org.wise.portal.service.chatbot; + +import java.util.List; +import java.util.Map; + +/** + * Service interface for managing chatbot conversations + * + * @author Hiroki Terashima + */ +public interface ChatbotService { + + /** + * Get all chats for a specific run and workgroup + * + * @param runId the run ID + * @param workgroupId the workgroup ID + * @return list of all chats + */ + List> getAllChats(Long runId, Long workgroupId); + + /** + * Get a specific chat by ID + * + * @param runId the run ID + * @param workgroupId the workgroup ID + * @param chatId the chat ID + * @return the requested chat + */ + Map getChat(Long runId, Long workgroupId, String chatId); + + /** + * Create a new chat + * + * @param runId the run ID + * @param workgroupId the workgroup ID + * @param chatData the chat data + * @return the created chat with generated ID + */ + Map createChat(Long runId, Long workgroupId, Map chatData); + + /** + * Update an existing chat + * + * @param runId the run ID + * @param workgroupId the workgroup ID + * @param chatId the chat ID + * @param chatData the updated chat data + * @return the updated chat + */ + Map updateChat(Long runId, Long workgroupId, String chatId, + Map chatData); + + /** + * Delete a chat + * + * @param runId the run ID + * @param workgroupId the workgroup ID + * @param chatId the chat ID + * @return success response + */ + Map deleteChat(Long runId, Long workgroupId, String chatId); +} diff --git a/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java b/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java new file mode 100644 index 000000000..042739b81 --- /dev/null +++ b/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java @@ -0,0 +1,159 @@ +package org.wise.portal.service.chatbot.impl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.stereotype.Service; +import org.wise.portal.service.chatbot.ChatbotService; + +/** + * Implementation of ChatbotService for managing chatbot conversations + * + * @author Hiroki Terashima + */ +@Service +public class ChatbotServiceImpl implements ChatbotService { + + @Override + public List> getAllChats(Long runId, Long workgroupId) { + List> chats = new ArrayList<>(); + + // Dummy chat 1 + Map chat1 = new HashMap<>(); + chat1.put("id", "chat_1766593333094"); + chat1.put("title", "everest"); + chat1.put("createdAt", "2025-12-24T16:22:13.094Z"); + chat1.put("lastUpdated", "2025-12-24T16:26:57.513Z"); + + List> messages1 = new ArrayList<>(); + messages1 + .add(createMessage("system", "You are a helpful assistant. Be polite and concise.", null)); + messages1.add(createMessage("user", "how tall is everest?", "2025-12-24T16:26:07.027Z")); + messages1.add(createMessage("assistant", + "Mount Everest's current official height is 8,848.86 meters (29,031.7 feet) above sea level.", + "2025-12-24T16:26:07.572Z")); + messages1.add(createMessage("user", "where is it?", "2025-12-24T16:26:57.020Z")); + messages1.add(createMessage("assistant", + "Mount Everest is located on the border between Nepal and China (Tibet Autonomous Region).", + "2025-12-24T16:26:57.513Z")); + chat1.put("messages", messages1); + + // Dummy chat 2 + Map chat2 = new HashMap<>(); + chat2.put("id", "chat_1766593571381"); + chat2.put("title", "k2"); + chat2.put("createdAt", "2025-12-24T16:26:11.381Z"); + chat2.put("lastUpdated", "2025-12-24T16:26:28.333Z"); + + List> messages2 = new ArrayList<>(); + messages2 + .add(createMessage("system", "You are a helpful assistant. Be polite and concise.", null)); + messages2.add(createMessage("user", "how tall is m2?", "2025-12-24T16:26:16.485Z")); + messages2.add(createMessage("assistant", + "The height of an M2 building can vary depending on the specific design and construction of the building. If you have a particular M2 building in mind, I can try to look up the information for you.", + "2025-12-24T16:26:18.034Z")); + chat2.put("messages", messages2); + + // Dummy chat 3 + Map chat3 = new HashMap<>(); + chat3.put("id", "chat_1766593589760"); + chat3.put("title", "berkeley"); + chat3.put("createdAt", "2025-12-24T16:26:29.760Z"); + chat3.put("lastUpdated", "2025-12-24T16:26:41.855Z"); + + List> messages3 = new ArrayList<>(); + messages3 + .add(createMessage("system", "You are a helpful assistant. Be polite and concise.", null)); + messages3.add(createMessage("user", "where is berkeley?", "2025-12-24T16:26:33.778Z")); + messages3.add(createMessage("assistant", + "Berkeley is a city located in the state of California in the United States.", + "2025-12-24T16:26:34.368Z")); + chat3.put("messages", messages3); + + chats.add(chat1); + chats.add(chat2); + chats.add(chat3); + + return chats; + } + + @Override + public Map getChat(Long runId, Long workgroupId, String chatId) { + Map chat = new HashMap<>(); + chat.put("id", chatId); + chat.put("title", "Sample Chat"); + chat.put("createdAt", "2025-12-24T16:22:13.094Z"); + chat.put("lastUpdated", "2025-12-24T16:26:57.513Z"); + + List> messages = new ArrayList<>(); + messages + .add(createMessage("system", "You are a helpful assistant. Be polite and concise.", null)); + messages.add(createMessage("user", "Hello!", "2025-12-24T16:26:07.027Z")); + messages.add( + createMessage("assistant", "Hello! How can I help you today?", "2025-12-24T16:26:07.572Z")); + chat.put("messages", messages); + + return chat; + } + + @Override + public Map createChat(Long runId, Long workgroupId, + Map chatData) { + Map createdChat = new HashMap<>(chatData); + + // Generate a dummy ID if not provided + if (!createdChat.containsKey("id")) { + createdChat.put("id", "chat_" + System.currentTimeMillis()); + } + + // Add timestamps if not provided + String now = java.time.Instant.now().toString(); + if (!createdChat.containsKey("createdAt")) { + createdChat.put("createdAt", now); + } + if (!createdChat.containsKey("lastUpdated")) { + createdChat.put("lastUpdated", now); + } + + return createdChat; + } + + @Override + public Map updateChat(Long runId, Long workgroupId, String chatId, + Map chatData) { + Map updatedChat = new HashMap<>(chatData); + updatedChat.put("id", chatId); + updatedChat.put("lastUpdated", java.time.Instant.now().toString()); + + return updatedChat; + } + + @Override + public Map deleteChat(Long runId, Long workgroupId, String chatId) { + Map response = new HashMap<>(); + response.put("message", "Chat deleted successfully"); + response.put("chatId", chatId); + + return response; + } + + /** + * Helper method to create a message object + * + * @param role the role (system, user, assistant) + * @param content the message content + * @param timestamp the timestamp (optional) + * @return the message map + */ + private Map createMessage(String role, String content, String timestamp) { + Map message = new HashMap<>(); + message.put("role", role); + message.put("content", content); + if (timestamp != null) { + message.put("timestamp", timestamp); + } + return message; + } +} From 0fdbad75a7eb3bba56ff0c0a6d41da20b18675e7 Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Mon, 29 Dec 2025 10:51:25 -0800 Subject: [PATCH 02/11] Create Chat domain object and update code to use it --- .../web/controllers/ChatbotController.java | 32 ++-- .../service/chatbot/ChatbotService.java | 19 +- .../chatbot/impl/ChatbotServiceImpl.java | 173 ++++++++---------- .../org/wise/vle/domain/chatbot/Chat.java | 107 +++++++++++ .../wise/vle/domain/chatbot/ChatMessage.java | 77 ++++++++ src/main/resources/wise_db_init.sql | 25 +++ 6 files changed, 315 insertions(+), 118 deletions(-) create mode 100644 src/main/java/org/wise/vle/domain/chatbot/Chat.java create mode 100644 src/main/java/org/wise/vle/domain/chatbot/ChatMessage.java diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/ChatbotController.java b/src/main/java/org/wise/portal/presentation/web/controllers/ChatbotController.java index dc13a41df..bcec97600 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/ChatbotController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/ChatbotController.java @@ -1,7 +1,6 @@ package org.wise.portal.presentation.web.controllers; import java.util.List; -import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -16,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.wise.portal.service.chatbot.ChatbotService; +import org.wise.vle.domain.chatbot.Chat; /** * REST controller for managing chatbot conversations @@ -38,7 +38,7 @@ public class ChatbotController { * @return list of all chats */ @GetMapping("/chats/{runId}/{workgroupId}") - public ResponseEntity>> getAllChats(@PathVariable Long runId, + public ResponseEntity> getAllChats(@PathVariable Long runId, @PathVariable Long workgroupId) { return ResponseEntity.ok(chatbotService.getAllChats(runId, workgroupId)); } @@ -52,8 +52,8 @@ public ResponseEntity>> getAllChats(@PathVariable Long * @return the requested chat */ @GetMapping("/chats/{runId}/{workgroupId}/{chatId}") - public ResponseEntity> getChat(@PathVariable Long runId, - @PathVariable Long workgroupId, @PathVariable String chatId) { + public ResponseEntity getChat(@PathVariable Long runId, @PathVariable Long workgroupId, + @PathVariable Long chatId) { return ResponseEntity.ok(chatbotService.getChat(runId, workgroupId, chatId)); } @@ -62,14 +62,14 @@ public ResponseEntity> getChat(@PathVariable Long runId, * * @param runId the run ID * @param workgroupId the workgroup ID - * @param chatData the chat data + * @param chat the chat data * @return the created chat with generated ID */ @PostMapping("/chats/{runId}/{workgroupId}") - public ResponseEntity> createChat(@PathVariable Long runId, - @PathVariable Long workgroupId, @RequestBody Map chatData) { + public ResponseEntity createChat(@PathVariable Long runId, @PathVariable Long workgroupId, + @RequestBody Chat chat) { return ResponseEntity.status(HttpStatus.CREATED) - .body(chatbotService.createChat(runId, workgroupId, chatData)); + .body(chatbotService.createChat(runId, workgroupId, chat)); } /** @@ -78,14 +78,13 @@ public ResponseEntity> createChat(@PathVariable Long runId, * @param runId the run ID * @param workgroupId the workgroup ID * @param chatId the chat ID - * @param chatData the updated chat data + * @param chat the updated chat data * @return the updated chat */ @PutMapping("/chats/{runId}/{workgroupId}/{chatId}") - public ResponseEntity> updateChat(@PathVariable Long runId, - @PathVariable Long workgroupId, @PathVariable String chatId, - @RequestBody Map chatData) { - return ResponseEntity.ok(chatbotService.updateChat(runId, workgroupId, chatId, chatData)); + public ResponseEntity updateChat(@PathVariable Long runId, @PathVariable Long workgroupId, + @PathVariable Long chatId, @RequestBody Chat chat) { + return ResponseEntity.ok(chatbotService.updateChat(runId, workgroupId, chatId, chat)); } /** @@ -97,8 +96,9 @@ public ResponseEntity> updateChat(@PathVariable Long runId, * @return success response */ @DeleteMapping("/chats/{runId}/{workgroupId}/{chatId}") - public ResponseEntity> deleteChat(@PathVariable Long runId, - @PathVariable Long workgroupId, @PathVariable String chatId) { - return ResponseEntity.ok(chatbotService.deleteChat(runId, workgroupId, chatId)); + public ResponseEntity deleteChat(@PathVariable Long runId, @PathVariable Long workgroupId, + @PathVariable Long chatId) { + chatbotService.deleteChat(runId, workgroupId, chatId); + return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/org/wise/portal/service/chatbot/ChatbotService.java b/src/main/java/org/wise/portal/service/chatbot/ChatbotService.java index c6e5b2c5a..150f207ca 100644 --- a/src/main/java/org/wise/portal/service/chatbot/ChatbotService.java +++ b/src/main/java/org/wise/portal/service/chatbot/ChatbotService.java @@ -1,7 +1,8 @@ package org.wise.portal.service.chatbot; import java.util.List; -import java.util.Map; + +import org.wise.vle.domain.chatbot.Chat; /** * Service interface for managing chatbot conversations @@ -17,7 +18,7 @@ public interface ChatbotService { * @param workgroupId the workgroup ID * @return list of all chats */ - List> getAllChats(Long runId, Long workgroupId); + List getAllChats(Long runId, Long workgroupId); /** * Get a specific chat by ID @@ -27,17 +28,17 @@ public interface ChatbotService { * @param chatId the chat ID * @return the requested chat */ - Map getChat(Long runId, Long workgroupId, String chatId); + Chat getChat(Long runId, Long workgroupId, Long chatId); /** * Create a new chat * * @param runId the run ID * @param workgroupId the workgroup ID - * @param chatData the chat data + * @param chat the chat data * @return the created chat with generated ID */ - Map createChat(Long runId, Long workgroupId, Map chatData); + Chat createChat(Long runId, Long workgroupId, Chat chat); /** * Update an existing chat @@ -45,11 +46,10 @@ public interface ChatbotService { * @param runId the run ID * @param workgroupId the workgroup ID * @param chatId the chat ID - * @param chatData the updated chat data + * @param chat the updated chat data * @return the updated chat */ - Map updateChat(Long runId, Long workgroupId, String chatId, - Map chatData); + Chat updateChat(Long runId, Long workgroupId, Long chatId, Chat chat); /** * Delete a chat @@ -57,7 +57,6 @@ Map updateChat(Long runId, Long workgroupId, String chatId, * @param runId the run ID * @param workgroupId the workgroup ID * @param chatId the chat ID - * @return success response */ - Map deleteChat(Long runId, Long workgroupId, String chatId); + void deleteChat(Long runId, Long workgroupId, Long chatId); } diff --git a/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java b/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java index 042739b81..08fc6fc16 100644 --- a/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java +++ b/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java @@ -1,12 +1,14 @@ package org.wise.portal.service.chatbot.impl; +import java.sql.Timestamp; +import java.time.Instant; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import org.springframework.stereotype.Service; import org.wise.portal.service.chatbot.ChatbotService; +import org.wise.vle.domain.chatbot.Chat; +import org.wise.vle.domain.chatbot.ChatMessage; /** * Implementation of ChatbotService for managing chatbot conversations @@ -17,60 +19,58 @@ public class ChatbotServiceImpl implements ChatbotService { @Override - public List> getAllChats(Long runId, Long workgroupId) { - List> chats = new ArrayList<>(); + public List getAllChats(Long runId, Long workgroupId) { + List chats = new ArrayList<>(); // Dummy chat 1 - Map chat1 = new HashMap<>(); - chat1.put("id", "chat_1766593333094"); - chat1.put("title", "everest"); - chat1.put("createdAt", "2025-12-24T16:22:13.094Z"); - chat1.put("lastUpdated", "2025-12-24T16:26:57.513Z"); - - List> messages1 = new ArrayList<>(); - messages1 - .add(createMessage("system", "You are a helpful assistant. Be polite and concise.", null)); - messages1.add(createMessage("user", "how tall is everest?", "2025-12-24T16:26:07.027Z")); - messages1.add(createMessage("assistant", + Chat chat1 = new Chat(); + chat1.setId(1766593333094L); + chat1.setTitle("everest"); + chat1.setCreatedAt(Timestamp.from(Instant.parse("2025-12-24T16:22:13.094Z"))); + chat1.setLastUpdated(Timestamp.from(Instant.parse("2025-12-24T16:26:57.513Z"))); + + chat1.addMessage( + createMessage("system", "You are a helpful assistant. Be polite and concise.", null)); + chat1.addMessage(createMessage("user", "how tall is everest?", + Timestamp.from(Instant.parse("2025-12-24T16:26:07.027Z")))); + chat1.addMessage(createMessage("assistant", "Mount Everest's current official height is 8,848.86 meters (29,031.7 feet) above sea level.", - "2025-12-24T16:26:07.572Z")); - messages1.add(createMessage("user", "where is it?", "2025-12-24T16:26:57.020Z")); - messages1.add(createMessage("assistant", + Timestamp.from(Instant.parse("2025-12-24T16:26:07.572Z")))); + chat1.addMessage(createMessage("user", "where is it?", + Timestamp.from(Instant.parse("2025-12-24T16:26:57.020Z")))); + chat1.addMessage(createMessage("assistant", "Mount Everest is located on the border between Nepal and China (Tibet Autonomous Region).", - "2025-12-24T16:26:57.513Z")); - chat1.put("messages", messages1); + Timestamp.from(Instant.parse("2025-12-24T16:26:57.513Z")))); // Dummy chat 2 - Map chat2 = new HashMap<>(); - chat2.put("id", "chat_1766593571381"); - chat2.put("title", "k2"); - chat2.put("createdAt", "2025-12-24T16:26:11.381Z"); - chat2.put("lastUpdated", "2025-12-24T16:26:28.333Z"); - - List> messages2 = new ArrayList<>(); - messages2 - .add(createMessage("system", "You are a helpful assistant. Be polite and concise.", null)); - messages2.add(createMessage("user", "how tall is m2?", "2025-12-24T16:26:16.485Z")); - messages2.add(createMessage("assistant", + Chat chat2 = new Chat(); + chat2.setId(1766593571381L); + chat2.setTitle("k2"); + chat2.setCreatedAt(Timestamp.from(Instant.parse("2025-12-24T16:26:11.381Z"))); + chat2.setLastUpdated(Timestamp.from(Instant.parse("2025-12-24T16:26:28.333Z"))); + + chat2.addMessage( + createMessage("system", "You are a helpful assistant. Be polite and concise.", null)); + chat2.addMessage(createMessage("user", "how tall is m2?", + Timestamp.from(Instant.parse("2025-12-24T16:26:16.485Z")))); + chat2.addMessage(createMessage("assistant", "The height of an M2 building can vary depending on the specific design and construction of the building. If you have a particular M2 building in mind, I can try to look up the information for you.", - "2025-12-24T16:26:18.034Z")); - chat2.put("messages", messages2); + Timestamp.from(Instant.parse("2025-12-24T16:26:18.034Z")))); // Dummy chat 3 - Map chat3 = new HashMap<>(); - chat3.put("id", "chat_1766593589760"); - chat3.put("title", "berkeley"); - chat3.put("createdAt", "2025-12-24T16:26:29.760Z"); - chat3.put("lastUpdated", "2025-12-24T16:26:41.855Z"); - - List> messages3 = new ArrayList<>(); - messages3 - .add(createMessage("system", "You are a helpful assistant. Be polite and concise.", null)); - messages3.add(createMessage("user", "where is berkeley?", "2025-12-24T16:26:33.778Z")); - messages3.add(createMessage("assistant", + Chat chat3 = new Chat(); + chat3.setId(1766593589760L); + chat3.setTitle("berkeley"); + chat3.setCreatedAt(Timestamp.from(Instant.parse("2025-12-24T16:26:29.760Z"))); + chat3.setLastUpdated(Timestamp.from(Instant.parse("2025-12-24T16:26:41.855Z"))); + + chat3.addMessage( + createMessage("system", "You are a helpful assistant. Be polite and concise.", null)); + chat3.addMessage(createMessage("user", "where is berkeley?", + Timestamp.from(Instant.parse("2025-12-24T16:26:33.778Z")))); + chat3.addMessage(createMessage("assistant", "Berkeley is a city located in the state of California in the United States.", - "2025-12-24T16:26:34.368Z")); - chat3.put("messages", messages3); + Timestamp.from(Instant.parse("2025-12-24T16:26:34.368Z")))); chats.add(chat1); chats.add(chat2); @@ -80,63 +80,54 @@ public List> getAllChats(Long runId, Long workgroupId) { } @Override - public Map getChat(Long runId, Long workgroupId, String chatId) { - Map chat = new HashMap<>(); - chat.put("id", chatId); - chat.put("title", "Sample Chat"); - chat.put("createdAt", "2025-12-24T16:22:13.094Z"); - chat.put("lastUpdated", "2025-12-24T16:26:57.513Z"); - - List> messages = new ArrayList<>(); - messages - .add(createMessage("system", "You are a helpful assistant. Be polite and concise.", null)); - messages.add(createMessage("user", "Hello!", "2025-12-24T16:26:07.027Z")); - messages.add( - createMessage("assistant", "Hello! How can I help you today?", "2025-12-24T16:26:07.572Z")); - chat.put("messages", messages); + public Chat getChat(Long runId, Long workgroupId, Long chatId) { + Chat chat = new Chat(); + chat.setId(chatId); + chat.setTitle("Sample Chat"); + chat.setCreatedAt(Timestamp.from(Instant.parse("2025-12-24T16:22:13.094Z"))); + chat.setLastUpdated(Timestamp.from(Instant.parse("2025-12-24T16:26:57.513Z"))); + + chat.addMessage( + createMessage("system", "You are a helpful assistant. Be polite and concise.", null)); + chat.addMessage( + createMessage("user", "Hello!", Timestamp.from(Instant.parse("2025-12-24T16:26:07.027Z")))); + chat.addMessage(createMessage("assistant", "Hello! How can I help you today?", + Timestamp.from(Instant.parse("2025-12-24T16:26:07.572Z")))); return chat; } @Override - public Map createChat(Long runId, Long workgroupId, - Map chatData) { - Map createdChat = new HashMap<>(chatData); - + public Chat createChat(Long runId, Long workgroupId, Chat chat) { // Generate a dummy ID if not provided - if (!createdChat.containsKey("id")) { - createdChat.put("id", "chat_" + System.currentTimeMillis()); + if (chat.getId() == null) { + chat.setId(System.currentTimeMillis()); } // Add timestamps if not provided - String now = java.time.Instant.now().toString(); - if (!createdChat.containsKey("createdAt")) { - createdChat.put("createdAt", now); + Timestamp now = Timestamp.from(Instant.now()); + if (chat.getCreatedAt() == null) { + chat.setCreatedAt(now); } - if (!createdChat.containsKey("lastUpdated")) { - createdChat.put("lastUpdated", now); + if (chat.getLastUpdated() == null) { + chat.setLastUpdated(now); } - return createdChat; + return chat; } @Override - public Map updateChat(Long runId, Long workgroupId, String chatId, - Map chatData) { - Map updatedChat = new HashMap<>(chatData); - updatedChat.put("id", chatId); - updatedChat.put("lastUpdated", java.time.Instant.now().toString()); + public Chat updateChat(Long runId, Long workgroupId, Long chatId, Chat chat) { + chat.setId(chatId); + chat.setLastUpdated(Timestamp.from(Instant.now())); - return updatedChat; + return chat; } @Override - public Map deleteChat(Long runId, Long workgroupId, String chatId) { - Map response = new HashMap<>(); - response.put("message", "Chat deleted successfully"); - response.put("chatId", chatId); - - return response; + public void deleteChat(Long runId, Long workgroupId, Long chatId) { + // In a real implementation, this would delete from the database + // For now, this is just a dummy implementation } /** @@ -145,15 +136,13 @@ public Map deleteChat(Long runId, Long workgroupId, String chatI * @param role the role (system, user, assistant) * @param content the message content * @param timestamp the timestamp (optional) - * @return the message map + * @return the message object */ - private Map createMessage(String role, String content, String timestamp) { - Map message = new HashMap<>(); - message.put("role", role); - message.put("content", content); - if (timestamp != null) { - message.put("timestamp", timestamp); - } + private ChatMessage createMessage(String role, String content, Timestamp timestamp) { + ChatMessage message = new ChatMessage(); + message.setRole(role); + message.setContent(content); + message.setTimestamp(timestamp); return message; } } diff --git a/src/main/java/org/wise/vle/domain/chatbot/Chat.java b/src/main/java/org/wise/vle/domain/chatbot/Chat.java new file mode 100644 index 000000000..a4ea34e6c --- /dev/null +++ b/src/main/java/org/wise/vle/domain/chatbot/Chat.java @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2007-2025 Regents of the University of California (Regents). + * Created by WISE, Graduate School of Education, University of California, Berkeley. + * + * This software is distributed under the GNU General Public License, v3, + * or (at your option) any later version. + * + * Permission is hereby granted, without written agreement and without license + * or royalty fees, to use, copy, modify, and distribute this software and its + * documentation for any purpose, provided that the above copyright notice and + * the following two paragraphs appear in all copies of this software. + * + * REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED + * HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE + * MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + * IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, + * SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, + * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF + * REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.wise.vle.domain.chatbot; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import org.wise.portal.domain.run.Run; +import org.wise.portal.domain.run.impl.RunImpl; +import org.wise.portal.domain.workgroup.Workgroup; +import org.wise.portal.domain.workgroup.impl.WorkgroupImpl; +import org.wise.vle.domain.PersistableDomain; + +import javax.persistence.*; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +/** + * Domain object representing a chatbot conversation + * + * @author Hiroki Terashima + */ +@Entity +@Table(name = "chatbot_chats", indexes = { + @Index(columnList = "runId", name = "chatbotChatsRunIdIndex"), + @Index(columnList = "workgroupId", name = "chatbotChatsWorkgroupIdIndex") }) +@Getter +@Setter +public class Chat extends PersistableDomain { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id = null; + + @ManyToOne(targetEntity = RunImpl.class, cascade = { + CascadeType.PERSIST }, fetch = FetchType.LAZY) + @JoinColumn(name = "runId", nullable = false) + @JsonIgnore + private Run run; + + @ManyToOne(targetEntity = WorkgroupImpl.class, cascade = { + CascadeType.PERSIST }, fetch = FetchType.LAZY) + @JoinColumn(name = "workgroupId", nullable = false) + @JsonIgnore + private Workgroup workgroup; + + @Column(name = "title", length = 255, nullable = true) + private String title; + + @Column(name = "createdAt", nullable = false) + private Timestamp createdAt; + + @Column(name = "lastUpdated", nullable = false) + private Timestamp lastUpdated; + + @OneToMany(mappedBy = "chat", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + @OrderBy("timestamp ASC") + private List messages = new ArrayList<>(); + + @Transient + private Long runId; + + @Transient + private Long workgroupId; + + @Override + protected Class getObjectClass() { + return Chat.class; + } + + public void convertToClientChat() { + this.setRunId(this.getRun().getId()); + this.setWorkgroupId(this.getWorkgroup().getId()); + } + + public void addMessage(ChatMessage message) { + messages.add(message); + message.setChat(this); + } + + public void removeMessage(ChatMessage message) { + messages.remove(message); + message.setChat(null); + } +} diff --git a/src/main/java/org/wise/vle/domain/chatbot/ChatMessage.java b/src/main/java/org/wise/vle/domain/chatbot/ChatMessage.java new file mode 100644 index 000000000..af4f67da3 --- /dev/null +++ b/src/main/java/org/wise/vle/domain/chatbot/ChatMessage.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2007-2025 Regents of the University of California (Regents). + * Created by WISE, Graduate School of Education, University of California, Berkeley. + * + * This software is distributed under the GNU General Public License, v3, + * or (at your option) any later version. + * + * Permission is hereby granted, without written agreement and without license + * or royalty fees, to use, copy, modify, and distribute this software and its + * documentation for any purpose, provided that the above copyright notice and + * the following two paragraphs appear in all copies of this software. + * + * REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED + * HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE + * MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + * IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, + * SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, + * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF + * REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.wise.vle.domain.chatbot; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import org.wise.vle.domain.PersistableDomain; + +import javax.persistence.*; +import java.sql.Timestamp; + +/** + * Domain object representing a single message in a chatbot conversation + * + * @author Hiroki Terashima + */ +@Entity +@Table(name = "chatbot_messages", indexes = { + @Index(columnList = "chatId", name = "chatbotMessagesChatIdIndex") }) +@Getter +@Setter +public class ChatMessage extends PersistableDomain { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id = null; + + @ManyToOne(targetEntity = Chat.class, cascade = { CascadeType.PERSIST }, fetch = FetchType.LAZY) + @JoinColumn(name = "chatId", nullable = false) + @JsonIgnore + private Chat chat; + + @Column(name = "role", length = 20, nullable = false) + private String role; // "system", "user", or "assistant" + + @Column(name = "content", length = 65536, columnDefinition = "text", nullable = false) + private String content; + + @Column(name = "timestamp", nullable = true) + private Timestamp timestamp; + + @Transient + private Long chatId; + + @Override + protected Class getObjectClass() { + return ChatMessage.class; + } + + public void convertToClientChatMessage() { + if (this.getChat() != null) { + this.setChatId(this.getChat().getId()); + } + } +} diff --git a/src/main/resources/wise_db_init.sql b/src/main/resources/wise_db_init.sql index febe460de..1bc79688f 100644 --- a/src/main/resources/wise_db_init.sql +++ b/src/main/resources/wise_db_init.sql @@ -90,6 +90,31 @@ create table annotations ( primary key (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +create table chatbot_chats ( + id bigint not null auto_increment, + runId bigint not null, + workgroupId bigint not null, + title varchar(255), + createdAt datetime not null, + lastUpdated datetime not null, + index chatbotChatsRunIdIndex (runId), + index chatbotChatsWorkgroupIdIndex (workgroupId), + constraint chatbotChatsRunIdFK foreign key (runId) references runs (id), + constraint chatbotChatsWorkgroupIdFK foreign key (workgroupId) references workgroups (id), + primary key (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +create table chatbot_messages ( + id bigint not null auto_increment, + chatId bigint not null, + role varchar(20) not null, + content text not null, + timestamp datetime, + index chatbotMessagesChatIdIndex (chatId), + constraint chatbotMessagesChatIdFK foreign key (chatId) references chatbot_chats (id) on delete cascade, + primary key (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + create table events ( id integer not null auto_increment, category varchar(255) not null, From f1432089c61c111262df4f45443fed4a6c072d0d Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Mon, 29 Dec 2025 13:12:37 -0800 Subject: [PATCH 03/11] Add DAO classes for Chat and ChatMessage --- .../org/wise/portal/dao/chatbot/ChatDao.java | 56 +++++ .../portal/dao/chatbot/ChatMessageDao.java | 37 +++ .../dao/chatbot/impl/HibernateChatDao.java | 85 +++++++ .../chatbot/impl/HibernateChatMessageDao.java | 44 ++++ .../chatbot/impl/ChatbotServiceImpl.java | 213 +++++++++--------- 5 files changed, 330 insertions(+), 105 deletions(-) create mode 100644 src/main/java/org/wise/portal/dao/chatbot/ChatDao.java create mode 100644 src/main/java/org/wise/portal/dao/chatbot/ChatMessageDao.java create mode 100644 src/main/java/org/wise/portal/dao/chatbot/impl/HibernateChatDao.java create mode 100644 src/main/java/org/wise/portal/dao/chatbot/impl/HibernateChatMessageDao.java diff --git a/src/main/java/org/wise/portal/dao/chatbot/ChatDao.java b/src/main/java/org/wise/portal/dao/chatbot/ChatDao.java new file mode 100644 index 000000000..85ce32522 --- /dev/null +++ b/src/main/java/org/wise/portal/dao/chatbot/ChatDao.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2007-2025 Regents of the University of California (Regents). + * Created by WISE, Graduate School of Education, University of California, Berkeley. + * + * This software is distributed under the GNU General Public License, v3, + * or (at your option) any later version. + * + * Permission is hereby granted, without written agreement and without license + * or royalty fees, to use, copy, modify, and distribute this software and its + * documentation for any purpose, provided that the above copyright notice and + * the following two paragraphs appear in all copies of this software. + * + * REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED + * HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE + * MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + * IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, + * SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, + * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF + * REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.wise.portal.dao.chatbot; + +import java.util.List; + +import org.wise.portal.dao.SimpleDao; +import org.wise.portal.domain.run.Run; +import org.wise.portal.domain.workgroup.Workgroup; +import org.wise.vle.domain.chatbot.Chat; + +/** + * Data Access Object interface for Chat + * + * @author Hiroki Terashima + */ +public interface ChatDao extends SimpleDao { + + /** + * Get all chats for a specific run and workgroup + * + * @param run the run + * @param workgroup the workgroup + * @return list of chats + */ + List getChatsByRunAndWorkgroup(Run run, Workgroup workgroup); + + /** + * Get a specific chat by ID + * + * @param id the chat ID + * @return the chat, or null if not found + */ + Chat getChatById(Long id); +} diff --git a/src/main/java/org/wise/portal/dao/chatbot/ChatMessageDao.java b/src/main/java/org/wise/portal/dao/chatbot/ChatMessageDao.java new file mode 100644 index 000000000..cf080e65a --- /dev/null +++ b/src/main/java/org/wise/portal/dao/chatbot/ChatMessageDao.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2007-2025 Regents of the University of California (Regents). + * Created by WISE, Graduate School of Education, University of California, Berkeley. + * + * This software is distributed under the GNU General Public License, v3, + * or (at your option) any later version. + * + * Permission is hereby granted, without written agreement and without license + * or royalty fees, to use, copy, modify, and distribute this software and its + * documentation for any purpose, provided that the above copyright notice and + * the following two paragraphs appear in all copies of this software. + * + * REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED + * HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE + * MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + * IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, + * SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, + * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF + * REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.wise.portal.dao.chatbot; + +import org.wise.portal.dao.SimpleDao; +import org.wise.vle.domain.chatbot.ChatMessage; + +/** + * Data Access Object interface for ChatMessage + * + * @author Hiroki Terashima + */ +public interface ChatMessageDao extends SimpleDao { + // ChatMessage operations are primarily handled through the Chat entity + // This interface extends SimpleDao for basic CRUD operations +} diff --git a/src/main/java/org/wise/portal/dao/chatbot/impl/HibernateChatDao.java b/src/main/java/org/wise/portal/dao/chatbot/impl/HibernateChatDao.java new file mode 100644 index 000000000..64ecb7b46 --- /dev/null +++ b/src/main/java/org/wise/portal/dao/chatbot/impl/HibernateChatDao.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2007-2025 Regents of the University of California (Regents). + * Created by WISE, Graduate School of Education, University of California, Berkeley. + * + * This software is distributed under the GNU General Public License, v3, + * or (at your option) any later version. + * + * Permission is hereby granted, without written agreement and without license + * or royalty fees, to use, copy, modify, and distribute this software and its + * documentation for any purpose, provided that the above copyright notice and + * the following two paragraphs appear in all copies of this software. + * + * REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED + * HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE + * MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + * IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, + * SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, + * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF + * REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.wise.portal.dao.chatbot.impl; + +import java.util.ArrayList; +import java.util.List; + +import javax.persistence.TypedQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + +import org.springframework.stereotype.Repository; +import org.wise.portal.dao.chatbot.ChatDao; +import org.wise.portal.dao.impl.AbstractHibernateDao; +import org.wise.portal.domain.run.Run; +import org.wise.portal.domain.workgroup.Workgroup; +import org.wise.vle.domain.chatbot.Chat; + +/** + * Hibernate implementation of ChatDao + * + * @author Hiroki Terashima + */ +@Repository +public class HibernateChatDao extends AbstractHibernateDao implements ChatDao { + + @Override + protected Class getDataObjectClass() { + return Chat.class; + } + + @Override + @SuppressWarnings("unchecked") + public List getChatsByRunAndWorkgroup(Run run, Workgroup workgroup) { + CriteriaBuilder cb = getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(Chat.class); + Root chatRoot = cq.from(Chat.class); + List predicates = new ArrayList<>(); + + if (run != null) { + predicates.add(cb.equal(chatRoot.get("run"), run)); + } + if (workgroup != null) { + predicates.add(cb.equal(chatRoot.get("workgroup"), workgroup)); + } + + cq.select(chatRoot).where(predicates.toArray(new Predicate[predicates.size()])) + .orderBy(cb.desc(chatRoot.get("lastUpdated"))); + + TypedQuery query = entityManager.createQuery(cq); + return (List) (Object) query.getResultList(); + } + + @Override + public Chat getChatById(Long id) { + try { + return getById(id); + } catch (Exception e) { + return null; + } + } +} diff --git a/src/main/java/org/wise/portal/dao/chatbot/impl/HibernateChatMessageDao.java b/src/main/java/org/wise/portal/dao/chatbot/impl/HibernateChatMessageDao.java new file mode 100644 index 000000000..c63ddb64a --- /dev/null +++ b/src/main/java/org/wise/portal/dao/chatbot/impl/HibernateChatMessageDao.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2007-2025 Regents of the University of California (Regents). + * Created by WISE, Graduate School of Education, University of California, Berkeley. + * + * This software is distributed under the GNU General Public License, v3, + * or (at your option) any later version. + * + * Permission is hereby granted, without written agreement and without license + * or royalty fees, to use, copy, modify, and distribute this software and its + * documentation for any purpose, provided that the above copyright notice and + * the following two paragraphs appear in all copies of this software. + * + * REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED + * HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE + * MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + * IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, + * SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, + * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF + * REGENTS HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.wise.portal.dao.chatbot.impl; + +import org.springframework.stereotype.Repository; +import org.wise.portal.dao.chatbot.ChatMessageDao; +import org.wise.portal.dao.impl.AbstractHibernateDao; +import org.wise.vle.domain.chatbot.ChatMessage; + +/** + * Hibernate implementation of ChatMessageDao + * + * @author Hiroki Terashima + */ +@Repository +public class HibernateChatMessageDao extends AbstractHibernateDao + implements ChatMessageDao { + + @Override + protected Class getDataObjectClass() { + return ChatMessage.class; + } +} diff --git a/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java b/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java index 08fc6fc16..a4cc27f53 100644 --- a/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java +++ b/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java @@ -2,10 +2,20 @@ import java.sql.Timestamp; import java.time.Instant; -import java.util.ArrayList; import java.util.List; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.wise.portal.dao.ObjectNotFoundException; +import org.wise.portal.dao.chatbot.ChatDao; +import org.wise.portal.dao.run.RunDao; +import org.wise.portal.dao.workgroup.WorkgroupDao; +import org.wise.portal.domain.run.Run; +import org.wise.portal.domain.workgroup.Workgroup; import org.wise.portal.service.chatbot.ChatbotService; import org.wise.vle.domain.chatbot.Chat; import org.wise.vle.domain.chatbot.ChatMessage; @@ -18,131 +28,124 @@ @Service public class ChatbotServiceImpl implements ChatbotService { + @PersistenceContext + private EntityManager entityManager; + + @Autowired + private ChatDao chatDao; + + @Autowired + private RunDao runDao; + + @Autowired + private WorkgroupDao workgroupDao; + @Override + @Transactional(readOnly = true) public List getAllChats(Long runId, Long workgroupId) { - List chats = new ArrayList<>(); - - // Dummy chat 1 - Chat chat1 = new Chat(); - chat1.setId(1766593333094L); - chat1.setTitle("everest"); - chat1.setCreatedAt(Timestamp.from(Instant.parse("2025-12-24T16:22:13.094Z"))); - chat1.setLastUpdated(Timestamp.from(Instant.parse("2025-12-24T16:26:57.513Z"))); - - chat1.addMessage( - createMessage("system", "You are a helpful assistant. Be polite and concise.", null)); - chat1.addMessage(createMessage("user", "how tall is everest?", - Timestamp.from(Instant.parse("2025-12-24T16:26:07.027Z")))); - chat1.addMessage(createMessage("assistant", - "Mount Everest's current official height is 8,848.86 meters (29,031.7 feet) above sea level.", - Timestamp.from(Instant.parse("2025-12-24T16:26:07.572Z")))); - chat1.addMessage(createMessage("user", "where is it?", - Timestamp.from(Instant.parse("2025-12-24T16:26:57.020Z")))); - chat1.addMessage(createMessage("assistant", - "Mount Everest is located on the border between Nepal and China (Tibet Autonomous Region).", - Timestamp.from(Instant.parse("2025-12-24T16:26:57.513Z")))); - - // Dummy chat 2 - Chat chat2 = new Chat(); - chat2.setId(1766593571381L); - chat2.setTitle("k2"); - chat2.setCreatedAt(Timestamp.from(Instant.parse("2025-12-24T16:26:11.381Z"))); - chat2.setLastUpdated(Timestamp.from(Instant.parse("2025-12-24T16:26:28.333Z"))); - - chat2.addMessage( - createMessage("system", "You are a helpful assistant. Be polite and concise.", null)); - chat2.addMessage(createMessage("user", "how tall is m2?", - Timestamp.from(Instant.parse("2025-12-24T16:26:16.485Z")))); - chat2.addMessage(createMessage("assistant", - "The height of an M2 building can vary depending on the specific design and construction of the building. If you have a particular M2 building in mind, I can try to look up the information for you.", - Timestamp.from(Instant.parse("2025-12-24T16:26:18.034Z")))); - - // Dummy chat 3 - Chat chat3 = new Chat(); - chat3.setId(1766593589760L); - chat3.setTitle("berkeley"); - chat3.setCreatedAt(Timestamp.from(Instant.parse("2025-12-24T16:26:29.760Z"))); - chat3.setLastUpdated(Timestamp.from(Instant.parse("2025-12-24T16:26:41.855Z"))); - - chat3.addMessage( - createMessage("system", "You are a helpful assistant. Be polite and concise.", null)); - chat3.addMessage(createMessage("user", "where is berkeley?", - Timestamp.from(Instant.parse("2025-12-24T16:26:33.778Z")))); - chat3.addMessage(createMessage("assistant", - "Berkeley is a city located in the state of California in the United States.", - Timestamp.from(Instant.parse("2025-12-24T16:26:34.368Z")))); - - chats.add(chat1); - chats.add(chat2); - chats.add(chat3); - - return chats; + try { + Run run = runDao.getById(runId); + Workgroup workgroup = workgroupDao.getById(workgroupId); + return chatDao.getChatsByRunAndWorkgroup(run, workgroup); + } catch (ObjectNotFoundException e) { + throw new IllegalArgumentException("Run or Workgroup not found", e); + } } @Override + @Transactional(readOnly = true) public Chat getChat(Long runId, Long workgroupId, Long chatId) { - Chat chat = new Chat(); - chat.setId(chatId); - chat.setTitle("Sample Chat"); - chat.setCreatedAt(Timestamp.from(Instant.parse("2025-12-24T16:22:13.094Z"))); - chat.setLastUpdated(Timestamp.from(Instant.parse("2025-12-24T16:26:57.513Z"))); - - chat.addMessage( - createMessage("system", "You are a helpful assistant. Be polite and concise.", null)); - chat.addMessage( - createMessage("user", "Hello!", Timestamp.from(Instant.parse("2025-12-24T16:26:07.027Z")))); - chat.addMessage(createMessage("assistant", "Hello! How can I help you today?", - Timestamp.from(Instant.parse("2025-12-24T16:26:07.572Z")))); - + Chat chat = chatDao.getChatById(chatId); + if (chat == null) { + throw new IllegalArgumentException("Chat not found with id: " + chatId); + } + // Verify the chat belongs to the specified run and workgroup + if (!chat.getRun().getId().equals(runId) || !chat.getWorkgroup().getId().equals(workgroupId)) { + throw new IllegalArgumentException("Chat does not belong to the specified run and workgroup"); + } return chat; } @Override + @Transactional public Chat createChat(Long runId, Long workgroupId, Chat chat) { - // Generate a dummy ID if not provided - if (chat.getId() == null) { - chat.setId(System.currentTimeMillis()); + try { + Run run = runDao.getById(runId); + Workgroup workgroup = workgroupDao.getById(workgroupId); + chat.setRun(run); + chat.setWorkgroup(workgroup); + + Timestamp now = Timestamp.from(Instant.now()); + if (chat.getCreatedAt() == null) { + chat.setCreatedAt(now); + } + if (chat.getLastUpdated() == null) { + chat.setLastUpdated(now); + } + if (chat.getMessages() != null) { + chat.getMessages().forEach(message -> message.setChat(chat)); + } + chatDao.save(chat); + return chat; + } catch (ObjectNotFoundException e) { + throw new IllegalArgumentException("Run or Workgroup not found", e); } + } - // Add timestamps if not provided - Timestamp now = Timestamp.from(Instant.now()); - if (chat.getCreatedAt() == null) { - chat.setCreatedAt(now); - } - if (chat.getLastUpdated() == null) { - chat.setLastUpdated(now); + @Override + @Transactional + public Chat updateChat(Long runId, Long workgroupId, Long chatId, Chat updatedChat) { + Chat existingChat = chatDao.getChatById(chatId); + if (existingChat == null) { + throw new IllegalArgumentException("Chat not found with id: " + chatId); } - return chat; - } + // Verify the chat belongs to the specified run and workgroup + if (!existingChat.getRun().getId().equals(runId) + || !existingChat.getWorkgroup().getId().equals(workgroupId)) { + throw new IllegalArgumentException("Chat does not belong to the specified run and workgroup"); + } - @Override - public Chat updateChat(Long runId, Long workgroupId, Long chatId, Chat chat) { - chat.setId(chatId); - chat.setLastUpdated(Timestamp.from(Instant.now())); + if (updatedChat.getTitle() != null) { + existingChat.setTitle(updatedChat.getTitle()); + } + existingChat.setLastUpdated(Timestamp.from(Instant.now())); + + if (updatedChat.getMessages() != null) { + existingChat.getMessages().clear(); + updatedChat.getMessages().forEach(message -> { + // If the message has an ID, it's an existing message that needs to be merged + // If it doesn't have an ID, it's a new message + if (message.getId() != null) { + // Merge the detached message back into the session + ChatMessage managedMessage = entityManager.merge(message); + managedMessage.setChat(existingChat); + existingChat.addMessage(managedMessage); + } else { + // New message - just set the chat reference + message.setChat(existingChat); + existingChat.addMessage(message); + } + }); + } - return chat; + chatDao.save(existingChat); + return existingChat; } @Override + @Transactional public void deleteChat(Long runId, Long workgroupId, Long chatId) { - // In a real implementation, this would delete from the database - // For now, this is just a dummy implementation - } + Chat chat = chatDao.getChatById(chatId); + if (chat == null) { + throw new IllegalArgumentException("Chat not found with id: " + chatId); + } + + // Verify the chat belongs to the specified run and workgroup + if (!chat.getRun().getId().equals(runId) || !chat.getWorkgroup().getId().equals(workgroupId)) { + throw new IllegalArgumentException("Chat does not belong to the specified run and workgroup"); + } - /** - * Helper method to create a message object - * - * @param role the role (system, user, assistant) - * @param content the message content - * @param timestamp the timestamp (optional) - * @return the message object - */ - private ChatMessage createMessage(String role, String content, Timestamp timestamp) { - ChatMessage message = new ChatMessage(); - message.setRole(role); - message.setContent(content); - message.setTimestamp(timestamp); - return message; + chatDao.delete(chat); } } From 40e77634d1102ccd418656fb17f12ea004b1b317 Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Mon, 29 Dec 2025 13:48:01 -0800 Subject: [PATCH 04/11] Soft delete Chats --- .../wise/portal/service/chatbot/impl/ChatbotServiceImpl.java | 4 ++-- src/main/java/org/wise/vle/domain/chatbot/Chat.java | 3 +++ src/main/resources/wise_db_init.sql | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java b/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java index a4cc27f53..137f32bce 100644 --- a/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java +++ b/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java @@ -145,7 +145,7 @@ public void deleteChat(Long runId, Long workgroupId, Long chatId) { if (!chat.getRun().getId().equals(runId) || !chat.getWorkgroup().getId().equals(workgroupId)) { throw new IllegalArgumentException("Chat does not belong to the specified run and workgroup"); } - - chatDao.delete(chat); + chat.setDeleted(true); + chatDao.save(chat); } } diff --git a/src/main/java/org/wise/vle/domain/chatbot/Chat.java b/src/main/java/org/wise/vle/domain/chatbot/Chat.java index a4ea34e6c..8c8876bf8 100644 --- a/src/main/java/org/wise/vle/domain/chatbot/Chat.java +++ b/src/main/java/org/wise/vle/domain/chatbot/Chat.java @@ -75,6 +75,9 @@ public class Chat extends PersistableDomain { @Column(name = "lastUpdated", nullable = false) private Timestamp lastUpdated; + @Column(name = "isDeleted", nullable = false) + private boolean isDeleted = false; + @OneToMany(mappedBy = "chat", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) @OrderBy("timestamp ASC") private List messages = new ArrayList<>(); diff --git a/src/main/resources/wise_db_init.sql b/src/main/resources/wise_db_init.sql index 1bc79688f..e11f2a3bd 100644 --- a/src/main/resources/wise_db_init.sql +++ b/src/main/resources/wise_db_init.sql @@ -97,6 +97,7 @@ create table chatbot_chats ( title varchar(255), createdAt datetime not null, lastUpdated datetime not null, + isDeleted bit not null default 0, index chatbotChatsRunIdIndex (runId), index chatbotChatsWorkgroupIdIndex (workgroupId), constraint chatbotChatsRunIdFK foreign key (runId) references runs (id), From 7a5d63ff9486ce6388c6fe7d39d7eb9e697f45d0 Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Mon, 29 Dec 2025 14:46:39 -0800 Subject: [PATCH 05/11] Clean up code. Use domain objects as input to controller endpoints. --- .../web/controllers/ChatbotController.java | 66 +++++++--------- .../service/chatbot/ChatbotService.java | 36 ++++----- .../chatbot/impl/ChatbotServiceImpl.java | 77 +++++-------------- 3 files changed, 62 insertions(+), 117 deletions(-) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/ChatbotController.java b/src/main/java/org/wise/portal/presentation/web/controllers/ChatbotController.java index bcec97600..69f07d873 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/ChatbotController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/ChatbotController.java @@ -14,6 +14,8 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.wise.portal.domain.run.impl.RunImpl; +import org.wise.portal.domain.workgroup.impl.WorkgroupImpl; import org.wise.portal.service.chatbot.ChatbotService; import org.wise.vle.domain.chatbot.Chat; @@ -33,72 +35,58 @@ public class ChatbotController { /** * Get all chats for a specific run and workgroup * - * @param runId the run ID - * @param workgroupId the workgroup ID + * @param run the run ID + * @param workgroup the workgroup ID * @return list of all chats */ - @GetMapping("/chats/{runId}/{workgroupId}") - public ResponseEntity> getAllChats(@PathVariable Long runId, - @PathVariable Long workgroupId) { - return ResponseEntity.ok(chatbotService.getAllChats(runId, workgroupId)); - } - - /** - * Get a specific chat by ID - * - * @param runId the run ID - * @param workgroupId the workgroup ID - * @param chatId the chat ID - * @return the requested chat - */ - @GetMapping("/chats/{runId}/{workgroupId}/{chatId}") - public ResponseEntity getChat(@PathVariable Long runId, @PathVariable Long workgroupId, - @PathVariable Long chatId) { - return ResponseEntity.ok(chatbotService.getChat(runId, workgroupId, chatId)); + @GetMapping("/chats/{run}/{workgroup}") + public ResponseEntity> getAllChats(@PathVariable RunImpl run, + @PathVariable WorkgroupImpl workgroup) { + return ResponseEntity.ok(chatbotService.getAllChats(run, workgroup)); } /** * Create a new chat * - * @param runId the run ID - * @param workgroupId the workgroup ID + * @param run the run ID + * @param workgroup the workgroup ID * @param chat the chat data - * @return the created chat with generated ID + * @return the created chat */ - @PostMapping("/chats/{runId}/{workgroupId}") - public ResponseEntity createChat(@PathVariable Long runId, @PathVariable Long workgroupId, - @RequestBody Chat chat) { + @PostMapping("/chats/{run}/{workgroup}") + public ResponseEntity createChat(@PathVariable RunImpl run, + @PathVariable WorkgroupImpl workgroup, @RequestBody Chat chat) { return ResponseEntity.status(HttpStatus.CREATED) - .body(chatbotService.createChat(runId, workgroupId, chat)); + .body(chatbotService.createChat(run, workgroup, chat)); } /** * Update an existing chat * - * @param runId the run ID - * @param workgroupId the workgroup ID + * @param run the run ID + * @param workgroup the workgroup ID * @param chatId the chat ID * @param chat the updated chat data * @return the updated chat */ - @PutMapping("/chats/{runId}/{workgroupId}/{chatId}") - public ResponseEntity updateChat(@PathVariable Long runId, @PathVariable Long workgroupId, - @PathVariable Long chatId, @RequestBody Chat chat) { - return ResponseEntity.ok(chatbotService.updateChat(runId, workgroupId, chatId, chat)); + @PutMapping("/chats/{run}/{workgroup}/{chatId}") + public ResponseEntity updateChat(@PathVariable RunImpl run, + @PathVariable WorkgroupImpl workgroup, @PathVariable Long chatId, @RequestBody Chat chat) { + return ResponseEntity.ok(chatbotService.updateChat(run, workgroup, chatId, chat)); } /** * Delete a chat * - * @param runId the run ID - * @param workgroupId the workgroup ID + * @param run the run ID + * @param workgroup the workgroup ID * @param chatId the chat ID * @return success response */ - @DeleteMapping("/chats/{runId}/{workgroupId}/{chatId}") - public ResponseEntity deleteChat(@PathVariable Long runId, @PathVariable Long workgroupId, - @PathVariable Long chatId) { - chatbotService.deleteChat(runId, workgroupId, chatId); + @DeleteMapping("/chats/{run}/{workgroup}/{chatId}") + public ResponseEntity deleteChat(@PathVariable RunImpl run, + @PathVariable WorkgroupImpl workgroup, @PathVariable Long chatId) { + chatbotService.deleteChat(run, workgroup, chatId); return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/org/wise/portal/service/chatbot/ChatbotService.java b/src/main/java/org/wise/portal/service/chatbot/ChatbotService.java index 150f207ca..fa5e6c183 100644 --- a/src/main/java/org/wise/portal/service/chatbot/ChatbotService.java +++ b/src/main/java/org/wise/portal/service/chatbot/ChatbotService.java @@ -2,6 +2,8 @@ import java.util.List; +import org.wise.portal.domain.run.Run; +import org.wise.portal.domain.workgroup.Workgroup; import org.wise.vle.domain.chatbot.Chat; /** @@ -14,49 +16,39 @@ public interface ChatbotService { /** * Get all chats for a specific run and workgroup * - * @param runId the run ID - * @param workgroupId the workgroup ID + * @param run the run + * @param workgroup the workgroup * @return list of all chats */ - List getAllChats(Long runId, Long workgroupId); - - /** - * Get a specific chat by ID - * - * @param runId the run ID - * @param workgroupId the workgroup ID - * @param chatId the chat ID - * @return the requested chat - */ - Chat getChat(Long runId, Long workgroupId, Long chatId); + List getAllChats(Run run, Workgroup workgroup); /** * Create a new chat * - * @param runId the run ID - * @param workgroupId the workgroup ID + * @param run the run + * @param workgroup the workgroup * @param chat the chat data * @return the created chat with generated ID */ - Chat createChat(Long runId, Long workgroupId, Chat chat); + Chat createChat(Run run, Workgroup workgroup, Chat chat); /** * Update an existing chat * - * @param runId the run ID - * @param workgroupId the workgroup ID + * @param run the run + * @param workgroup the workgroup * @param chatId the chat ID * @param chat the updated chat data * @return the updated chat */ - Chat updateChat(Long runId, Long workgroupId, Long chatId, Chat chat); + Chat updateChat(Run run, Workgroup workgroup, Long chatId, Chat chat); /** * Delete a chat * - * @param runId the run ID - * @param workgroupId the workgroup ID + * @param run the run + * @param workgroup the workgroup * @param chatId the chat ID */ - void deleteChat(Long runId, Long workgroupId, Long chatId); + void deleteChat(Run run, Workgroup workgroup, Long chatId); } diff --git a/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java b/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java index 137f32bce..502987733 100644 --- a/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java +++ b/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java @@ -10,10 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.wise.portal.dao.ObjectNotFoundException; import org.wise.portal.dao.chatbot.ChatDao; -import org.wise.portal.dao.run.RunDao; -import org.wise.portal.dao.workgroup.WorkgroupDao; import org.wise.portal.domain.run.Run; import org.wise.portal.domain.workgroup.Workgroup; import org.wise.portal.service.chatbot.ChatbotService; @@ -34,75 +31,43 @@ public class ChatbotServiceImpl implements ChatbotService { @Autowired private ChatDao chatDao; - @Autowired - private RunDao runDao; - - @Autowired - private WorkgroupDao workgroupDao; - @Override @Transactional(readOnly = true) - public List getAllChats(Long runId, Long workgroupId) { - try { - Run run = runDao.getById(runId); - Workgroup workgroup = workgroupDao.getById(workgroupId); - return chatDao.getChatsByRunAndWorkgroup(run, workgroup); - } catch (ObjectNotFoundException e) { - throw new IllegalArgumentException("Run or Workgroup not found", e); - } - } + public List getAllChats(Run run, Workgroup workgroup) { + return chatDao.getChatsByRunAndWorkgroup(run, workgroup); - @Override - @Transactional(readOnly = true) - public Chat getChat(Long runId, Long workgroupId, Long chatId) { - Chat chat = chatDao.getChatById(chatId); - if (chat == null) { - throw new IllegalArgumentException("Chat not found with id: " + chatId); - } - // Verify the chat belongs to the specified run and workgroup - if (!chat.getRun().getId().equals(runId) || !chat.getWorkgroup().getId().equals(workgroupId)) { - throw new IllegalArgumentException("Chat does not belong to the specified run and workgroup"); - } - return chat; } @Override @Transactional - public Chat createChat(Long runId, Long workgroupId, Chat chat) { - try { - Run run = runDao.getById(runId); - Workgroup workgroup = workgroupDao.getById(workgroupId); - chat.setRun(run); - chat.setWorkgroup(workgroup); - - Timestamp now = Timestamp.from(Instant.now()); - if (chat.getCreatedAt() == null) { - chat.setCreatedAt(now); - } - if (chat.getLastUpdated() == null) { - chat.setLastUpdated(now); - } - if (chat.getMessages() != null) { - chat.getMessages().forEach(message -> message.setChat(chat)); - } - chatDao.save(chat); - return chat; - } catch (ObjectNotFoundException e) { - throw new IllegalArgumentException("Run or Workgroup not found", e); + public Chat createChat(Run run, Workgroup workgroup, Chat chat) { + chat.setRun(run); + chat.setWorkgroup(workgroup); + + Timestamp now = Timestamp.from(Instant.now()); + if (chat.getCreatedAt() == null) { + chat.setCreatedAt(now); + } + if (chat.getLastUpdated() == null) { + chat.setLastUpdated(now); + } + if (chat.getMessages() != null) { + chat.getMessages().forEach(message -> message.setChat(chat)); } + chatDao.save(chat); + return chat; } @Override @Transactional - public Chat updateChat(Long runId, Long workgroupId, Long chatId, Chat updatedChat) { + public Chat updateChat(Run run, Workgroup workgroup, Long chatId, Chat updatedChat) { Chat existingChat = chatDao.getChatById(chatId); if (existingChat == null) { throw new IllegalArgumentException("Chat not found with id: " + chatId); } // Verify the chat belongs to the specified run and workgroup - if (!existingChat.getRun().getId().equals(runId) - || !existingChat.getWorkgroup().getId().equals(workgroupId)) { + if (!existingChat.getRun().equals(run) || !existingChat.getWorkgroup().equals(workgroup)) { throw new IllegalArgumentException("Chat does not belong to the specified run and workgroup"); } @@ -135,14 +100,14 @@ public Chat updateChat(Long runId, Long workgroupId, Long chatId, Chat updatedCh @Override @Transactional - public void deleteChat(Long runId, Long workgroupId, Long chatId) { + public void deleteChat(Run run, Workgroup workgroup, Long chatId) { Chat chat = chatDao.getChatById(chatId); if (chat == null) { throw new IllegalArgumentException("Chat not found with id: " + chatId); } // Verify the chat belongs to the specified run and workgroup - if (!chat.getRun().getId().equals(runId) || !chat.getWorkgroup().getId().equals(workgroupId)) { + if (!chat.getRun().equals(run) || !chat.getWorkgroup().equals(workgroup)) { throw new IllegalArgumentException("Chat does not belong to the specified run and workgroup"); } chat.setDeleted(true); From 4e180710f9197c5f38e6b536c70c9f6a7e197134 Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Mon, 29 Dec 2025 15:04:43 -0800 Subject: [PATCH 06/11] Extract method refactor --- .../service/chatbot/impl/ChatbotServiceImpl.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java b/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java index 502987733..edb59edb4 100644 --- a/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java +++ b/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java @@ -35,7 +35,6 @@ public class ChatbotServiceImpl implements ChatbotService { @Transactional(readOnly = true) public List getAllChats(Run run, Workgroup workgroup) { return chatDao.getChatsByRunAndWorkgroup(run, workgroup); - } @Override @@ -65,12 +64,7 @@ public Chat updateChat(Run run, Workgroup workgroup, Long chatId, Chat updatedCh if (existingChat == null) { throw new IllegalArgumentException("Chat not found with id: " + chatId); } - - // Verify the chat belongs to the specified run and workgroup - if (!existingChat.getRun().equals(run) || !existingChat.getWorkgroup().equals(workgroup)) { - throw new IllegalArgumentException("Chat does not belong to the specified run and workgroup"); - } - + validateChatOwnership(existingChat, run, workgroup); if (updatedChat.getTitle() != null) { existingChat.setTitle(updatedChat.getTitle()); } @@ -105,12 +99,14 @@ public void deleteChat(Run run, Workgroup workgroup, Long chatId) { if (chat == null) { throw new IllegalArgumentException("Chat not found with id: " + chatId); } + validateChatOwnership(chat, run, workgroup); + chat.setDeleted(true); + chatDao.save(chat); + } - // Verify the chat belongs to the specified run and workgroup + private void validateChatOwnership(Chat chat, Run run, Workgroup workgroup) { if (!chat.getRun().equals(run) || !chat.getWorkgroup().equals(workgroup)) { throw new IllegalArgumentException("Chat does not belong to the specified run and workgroup"); } - chat.setDeleted(true); - chatDao.save(chat); } } From 9e00df82bd0c57d44cd44bbcd486af9756c1f27b Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Mon, 29 Dec 2025 16:10:45 -0800 Subject: [PATCH 07/11] Add nodeId field to ChatMessage --- src/main/java/org/wise/vle/domain/chatbot/ChatMessage.java | 3 +++ src/main/resources/wise_db_init.sql | 1 + 2 files changed, 4 insertions(+) diff --git a/src/main/java/org/wise/vle/domain/chatbot/ChatMessage.java b/src/main/java/org/wise/vle/domain/chatbot/ChatMessage.java index af4f67da3..9c5901a5f 100644 --- a/src/main/java/org/wise/vle/domain/chatbot/ChatMessage.java +++ b/src/main/java/org/wise/vle/domain/chatbot/ChatMessage.java @@ -61,6 +61,9 @@ public class ChatMessage extends PersistableDomain { @Column(name = "timestamp", nullable = true) private Timestamp timestamp; + @Column(name = "nodeId", length = 30, nullable = true) + private String nodeId; + @Transient private Long chatId; diff --git a/src/main/resources/wise_db_init.sql b/src/main/resources/wise_db_init.sql index e11f2a3bd..dd1d2451c 100644 --- a/src/main/resources/wise_db_init.sql +++ b/src/main/resources/wise_db_init.sql @@ -111,6 +111,7 @@ create table chatbot_messages ( role varchar(20) not null, content text not null, timestamp datetime, + nodeId varchar(30), index chatbotMessagesChatIdIndex (chatId), constraint chatbotMessagesChatIdFK foreign key (chatId) references chatbot_chats (id) on delete cascade, primary key (id) From 6d980149ad456b4ad8031a3eafd35414c5af1a53 Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Tue, 30 Dec 2025 09:08:22 -0800 Subject: [PATCH 08/11] Clean up code --- .../org/wise/portal/dao/chatbot/ChatDao.java | 8 -------- .../dao/chatbot/impl/HibernateChatDao.java | 9 --------- .../web/controllers/ChatbotController.java | 8 ++++++-- .../portal/service/chatbot/ChatbotService.java | 10 +++++++--- .../service/chatbot/impl/ChatbotServiceImpl.java | 16 ++++++---------- 5 files changed, 19 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/wise/portal/dao/chatbot/ChatDao.java b/src/main/java/org/wise/portal/dao/chatbot/ChatDao.java index 85ce32522..43e0d7eb4 100644 --- a/src/main/java/org/wise/portal/dao/chatbot/ChatDao.java +++ b/src/main/java/org/wise/portal/dao/chatbot/ChatDao.java @@ -45,12 +45,4 @@ public interface ChatDao extends SimpleDao { * @return list of chats */ List getChatsByRunAndWorkgroup(Run run, Workgroup workgroup); - - /** - * Get a specific chat by ID - * - * @param id the chat ID - * @return the chat, or null if not found - */ - Chat getChatById(Long id); } diff --git a/src/main/java/org/wise/portal/dao/chatbot/impl/HibernateChatDao.java b/src/main/java/org/wise/portal/dao/chatbot/impl/HibernateChatDao.java index 64ecb7b46..23acf7e12 100644 --- a/src/main/java/org/wise/portal/dao/chatbot/impl/HibernateChatDao.java +++ b/src/main/java/org/wise/portal/dao/chatbot/impl/HibernateChatDao.java @@ -73,13 +73,4 @@ public List getChatsByRunAndWorkgroup(Run run, Workgroup workgroup) { TypedQuery query = entityManager.createQuery(cq); return (List) (Object) query.getResultList(); } - - @Override - public Chat getChatById(Long id) { - try { - return getById(id); - } catch (Exception e) { - return null; - } - } } diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/ChatbotController.java b/src/main/java/org/wise/portal/presentation/web/controllers/ChatbotController.java index 69f07d873..97cdc0bba 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/ChatbotController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/ChatbotController.java @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.wise.portal.dao.ObjectNotFoundException; import org.wise.portal.domain.run.impl.RunImpl; import org.wise.portal.domain.workgroup.impl.WorkgroupImpl; import org.wise.portal.service.chatbot.ChatbotService; @@ -68,10 +69,12 @@ public ResponseEntity createChat(@PathVariable RunImpl run, * @param chatId the chat ID * @param chat the updated chat data * @return the updated chat + * @throws ObjectNotFoundException when the chat is not found */ @PutMapping("/chats/{run}/{workgroup}/{chatId}") public ResponseEntity updateChat(@PathVariable RunImpl run, - @PathVariable WorkgroupImpl workgroup, @PathVariable Long chatId, @RequestBody Chat chat) { + @PathVariable WorkgroupImpl workgroup, @PathVariable Long chatId, @RequestBody Chat chat) + throws ObjectNotFoundException { return ResponseEntity.ok(chatbotService.updateChat(run, workgroup, chatId, chat)); } @@ -82,10 +85,11 @@ public ResponseEntity updateChat(@PathVariable RunImpl run, * @param workgroup the workgroup ID * @param chatId the chat ID * @return success response + * @throws ObjectNotFoundException when the chat is not found */ @DeleteMapping("/chats/{run}/{workgroup}/{chatId}") public ResponseEntity deleteChat(@PathVariable RunImpl run, - @PathVariable WorkgroupImpl workgroup, @PathVariable Long chatId) { + @PathVariable WorkgroupImpl workgroup, @PathVariable Long chatId) throws ObjectNotFoundException { chatbotService.deleteChat(run, workgroup, chatId); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/org/wise/portal/service/chatbot/ChatbotService.java b/src/main/java/org/wise/portal/service/chatbot/ChatbotService.java index fa5e6c183..4d1dadce0 100644 --- a/src/main/java/org/wise/portal/service/chatbot/ChatbotService.java +++ b/src/main/java/org/wise/portal/service/chatbot/ChatbotService.java @@ -2,6 +2,7 @@ import java.util.List; +import org.wise.portal.dao.ObjectNotFoundException; import org.wise.portal.domain.run.Run; import org.wise.portal.domain.workgroup.Workgroup; import org.wise.vle.domain.chatbot.Chat; @@ -28,7 +29,7 @@ public interface ChatbotService { * @param run the run * @param workgroup the workgroup * @param chat the chat data - * @return the created chat with generated ID + * @return the created chat */ Chat createChat(Run run, Workgroup workgroup, Chat chat); @@ -40,8 +41,10 @@ public interface ChatbotService { * @param chatId the chat ID * @param chat the updated chat data * @return the updated chat + * @throws ObjectNotFoundException when the chat is not found */ - Chat updateChat(Run run, Workgroup workgroup, Long chatId, Chat chat); + Chat updateChat(Run run, Workgroup workgroup, Long chatId, Chat chat) + throws ObjectNotFoundException; /** * Delete a chat @@ -49,6 +52,7 @@ public interface ChatbotService { * @param run the run * @param workgroup the workgroup * @param chatId the chat ID + * @throws ObjectNotFoundException when the chat is not found */ - void deleteChat(Run run, Workgroup workgroup, Long chatId); + void deleteChat(Run run, Workgroup workgroup, Long chatId) throws ObjectNotFoundException; } diff --git a/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java b/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java index edb59edb4..2ae1865ec 100644 --- a/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java +++ b/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java @@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.wise.portal.dao.ObjectNotFoundException; import org.wise.portal.dao.chatbot.ChatDao; import org.wise.portal.domain.run.Run; import org.wise.portal.domain.workgroup.Workgroup; @@ -59,11 +60,9 @@ public Chat createChat(Run run, Workgroup workgroup, Chat chat) { @Override @Transactional - public Chat updateChat(Run run, Workgroup workgroup, Long chatId, Chat updatedChat) { - Chat existingChat = chatDao.getChatById(chatId); - if (existingChat == null) { - throw new IllegalArgumentException("Chat not found with id: " + chatId); - } + public Chat updateChat(Run run, Workgroup workgroup, Long chatId, Chat updatedChat) + throws ObjectNotFoundException { + Chat existingChat = chatDao.getById(chatId); validateChatOwnership(existingChat, run, workgroup); if (updatedChat.getTitle() != null) { existingChat.setTitle(updatedChat.getTitle()); @@ -94,11 +93,8 @@ public Chat updateChat(Run run, Workgroup workgroup, Long chatId, Chat updatedCh @Override @Transactional - public void deleteChat(Run run, Workgroup workgroup, Long chatId) { - Chat chat = chatDao.getChatById(chatId); - if (chat == null) { - throw new IllegalArgumentException("Chat not found with id: " + chatId); - } + public void deleteChat(Run run, Workgroup workgroup, Long chatId) throws ObjectNotFoundException { + Chat chat = chatDao.getById(chatId); validateChatOwnership(chat, run, workgroup); chat.setDeleted(true); chatDao.save(chat); From 8eff1cf4001b41766d79104207174e7715c6294f Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Mon, 5 Jan 2026 11:44:56 -0800 Subject: [PATCH 09/11] Add AWSBedrock chat option --- .../web/AWSBedrockController.java | 64 +++++++++++++++++++ .../application-dockerdev-sample.properties | 4 ++ .../resources/application_sample.properties | 3 + 3 files changed, 71 insertions(+) create mode 100644 src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java diff --git a/src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java b/src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java new file mode 100644 index 000000000..f34715c46 --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java @@ -0,0 +1,64 @@ +package org.wise.portal.presentation.web; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.URL; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.security.access.annotation.Secured; +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.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/aws-bedrock/chat") +public class AWSBedrockController { + + @Autowired + Environment appProperties; + + @ResponseBody + @Secured("ROLE_USER") + @PostMapping + protected String sendChatMessage(@RequestBody String body) { + String awsBedrockApiKey = appProperties.getProperty("aws.bedrock.api.key"); + if (awsBedrockApiKey == null || awsBedrockApiKey.isEmpty()) { + throw new RuntimeException("aws.bedrock.api.key is not set"); + } + String awsBedrockApiUrl = appProperties.getProperty("aws.bedrock.api.url"); + if (awsBedrockApiUrl == null || awsBedrockApiUrl.isEmpty()) { + throw new RuntimeException("aws.bedrock.api.url is not set"); + } + try { + URL url = new URL(awsBedrockApiUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Authorization", "Bearer " + awsBedrockApiKey); + connection.setRequestProperty("Content-Type", "application/json; charset=utf-8"); + connection.setRequestProperty("Accept-Charset", "UTF-8"); + connection.setDoOutput(true); + OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream()); + writer.write(body); + writer.flush(); + writer.close(); + BufferedReader br = new BufferedReader( + new InputStreamReader(connection.getInputStream(), "UTF-8")); + String line; + StringBuffer response = new StringBuffer(); + while ((line = br.readLine()) != null) { + response.append(line); + } + br.close(); + return response.toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/resources/application-dockerdev-sample.properties b/src/main/resources/application-dockerdev-sample.properties index 6c97c6c28..65f903279 100644 --- a/src/main/resources/application-dockerdev-sample.properties +++ b/src/main/resources/application-dockerdev-sample.properties @@ -216,4 +216,8 @@ system-wide-salt=secret #speech-to-text.aws.region= #speech-to-text.aws.identity-pool-id= +# OpenAI and AWS Bedrock Chat endpoints (optional) #OPENAI_API_KEY= +#aws.bedrock.api.key= +#aws.bedrock.api.url= + diff --git a/src/main/resources/application_sample.properties b/src/main/resources/application_sample.properties index e0b196554..4248eff2a 100644 --- a/src/main/resources/application_sample.properties +++ b/src/main/resources/application_sample.properties @@ -216,4 +216,7 @@ system-wide-salt=secret #speech-to-text.aws.region= #speech-to-text.aws.identity-pool-id= +# OpenAI and AWS Bedrock Chat endpoints (optional) #OPENAI_API_KEY= +#aws.bedrock.api.key= +#aws.bedrock.api.url= From 452c4c325e7be3d03d5dc5433189c0694482f273 Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Mon, 5 Jan 2026 14:18:22 -0800 Subject: [PATCH 10/11] Made ChatMessage.nodeId non-nullable. Every message should be created on a step. --- src/main/java/org/wise/vle/domain/chatbot/ChatMessage.java | 2 +- src/main/resources/wise_db_init.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/wise/vle/domain/chatbot/ChatMessage.java b/src/main/java/org/wise/vle/domain/chatbot/ChatMessage.java index 9c5901a5f..27066b77e 100644 --- a/src/main/java/org/wise/vle/domain/chatbot/ChatMessage.java +++ b/src/main/java/org/wise/vle/domain/chatbot/ChatMessage.java @@ -61,7 +61,7 @@ public class ChatMessage extends PersistableDomain { @Column(name = "timestamp", nullable = true) private Timestamp timestamp; - @Column(name = "nodeId", length = 30, nullable = true) + @Column(name = "nodeId", length = 30, nullable = false) private String nodeId; @Transient diff --git a/src/main/resources/wise_db_init.sql b/src/main/resources/wise_db_init.sql index dd1d2451c..3e5cf652d 100644 --- a/src/main/resources/wise_db_init.sql +++ b/src/main/resources/wise_db_init.sql @@ -111,7 +111,7 @@ create table chatbot_messages ( role varchar(20) not null, content text not null, timestamp datetime, - nodeId varchar(30), + nodeId varchar(30) not null, index chatbotMessagesChatIdIndex (chatId), constraint chatbotMessagesChatIdFK foreign key (chatId) references chatbot_chats (id) on delete cascade, primary key (id) From 7919d9065388a1b0cf1dae2c530c02d5e7a9486c Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Mon, 5 Jan 2026 14:25:39 -0800 Subject: [PATCH 11/11] Rename api.url to runtime.endpoint to match AWS documentation. Figure out the model endpoing based on model passed in from client. For now, only support openai models. --- .../presentation/web/AWSBedrockController.java | 17 ++++++++++------- .../application-dockerdev-sample.properties | 2 +- .../resources/application_sample.properties | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java b/src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java index f34715c46..253c64c67 100644 --- a/src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java +++ b/src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java @@ -27,19 +27,22 @@ public class AWSBedrockController { @Secured("ROLE_USER") @PostMapping protected String sendChatMessage(@RequestBody String body) { - String awsBedrockApiKey = appProperties.getProperty("aws.bedrock.api.key"); - if (awsBedrockApiKey == null || awsBedrockApiKey.isEmpty()) { + String apiKey = appProperties.getProperty("aws.bedrock.api.key"); + if (apiKey == null || apiKey.isEmpty()) { throw new RuntimeException("aws.bedrock.api.key is not set"); } - String awsBedrockApiUrl = appProperties.getProperty("aws.bedrock.api.url"); - if (awsBedrockApiUrl == null || awsBedrockApiUrl.isEmpty()) { - throw new RuntimeException("aws.bedrock.api.url is not set"); + String apiEndpoint = appProperties.getProperty("aws.bedrock.runtime.endpoint"); + if (apiEndpoint == null || apiEndpoint.isEmpty()) { + throw new RuntimeException("aws.bedrock.runtime.endpoint is not set"); } + // assume openai-only support for now. We'll add other models later. + apiEndpoint += "/openai/v1/chat/completions"; + try { - URL url = new URL(awsBedrockApiUrl); + URL url = new URL(apiEndpoint); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("POST"); - connection.setRequestProperty("Authorization", "Bearer " + awsBedrockApiKey); + connection.setRequestProperty("Authorization", "Bearer " + apiKey); connection.setRequestProperty("Content-Type", "application/json; charset=utf-8"); connection.setRequestProperty("Accept-Charset", "UTF-8"); connection.setDoOutput(true); diff --git a/src/main/resources/application-dockerdev-sample.properties b/src/main/resources/application-dockerdev-sample.properties index 65f903279..d3b2b0a73 100644 --- a/src/main/resources/application-dockerdev-sample.properties +++ b/src/main/resources/application-dockerdev-sample.properties @@ -219,5 +219,5 @@ system-wide-salt=secret # OpenAI and AWS Bedrock Chat endpoints (optional) #OPENAI_API_KEY= #aws.bedrock.api.key= -#aws.bedrock.api.url= +#aws.bedrock.runtime.endpoint= diff --git a/src/main/resources/application_sample.properties b/src/main/resources/application_sample.properties index 4248eff2a..cba99ec23 100644 --- a/src/main/resources/application_sample.properties +++ b/src/main/resources/application_sample.properties @@ -219,4 +219,4 @@ system-wide-salt=secret # OpenAI and AWS Bedrock Chat endpoints (optional) #OPENAI_API_KEY= #aws.bedrock.api.key= -#aws.bedrock.api.url= +#aws.bedrock.runtime.endpoint=