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..43e0d7eb4 --- /dev/null +++ b/src/main/java/org/wise/portal/dao/chatbot/ChatDao.java @@ -0,0 +1,48 @@ +/** + * 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); +} 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..23acf7e12 --- /dev/null +++ b/src/main/java/org/wise/portal/dao/chatbot/impl/HibernateChatDao.java @@ -0,0 +1,76 @@ +/** + * 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(); + } +} 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/presentation/web/AWSBedrockController.java b/src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java new file mode 100644 index 000000000..253c64c67 --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java @@ -0,0 +1,67 @@ +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 apiKey = appProperties.getProperty("aws.bedrock.api.key"); + if (apiKey == null || apiKey.isEmpty()) { + throw new RuntimeException("aws.bedrock.api.key 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(apiEndpoint); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Authorization", "Bearer " + apiKey); + 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/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..97cdc0bba --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/controllers/ChatbotController.java @@ -0,0 +1,96 @@ +package org.wise.portal.presentation.web.controllers; + +import java.util.List; + +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.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; +import org.wise.vle.domain.chatbot.Chat; + +/** + * 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 run the run ID + * @param workgroup the workgroup ID + * @return list of all chats + */ + @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 run the run ID + * @param workgroup the workgroup ID + * @param chat the chat data + * @return the created 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(run, workgroup, chat)); + } + + /** + * Update an existing chat + * + * @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 + * @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) + throws ObjectNotFoundException { + return ResponseEntity.ok(chatbotService.updateChat(run, workgroup, chatId, chat)); + } + + /** + * Delete a chat + * + * @param run the run ID + * @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) 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 new file mode 100644 index 000000000..4d1dadce0 --- /dev/null +++ b/src/main/java/org/wise/portal/service/chatbot/ChatbotService.java @@ -0,0 +1,58 @@ +package org.wise.portal.service.chatbot; + +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; + +/** + * Service interface for managing chatbot conversations + * + * @author Hiroki Terashima + */ +public interface ChatbotService { + + /** + * Get all chats for a specific run and workgroup + * + * @param run the run + * @param workgroup the workgroup + * @return list of all chats + */ + List getAllChats(Run run, Workgroup workgroup); + + /** + * Create a new chat + * + * @param run the run + * @param workgroup the workgroup + * @param chat the chat data + * @return the created chat + */ + Chat createChat(Run run, Workgroup workgroup, Chat chat); + + /** + * Update an existing chat + * + * @param run the run + * @param workgroup the workgroup + * @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) + throws ObjectNotFoundException; + + /** + * Delete a chat + * + * @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) 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 new file mode 100644 index 000000000..2ae1865ec --- /dev/null +++ b/src/main/java/org/wise/portal/service/chatbot/impl/ChatbotServiceImpl.java @@ -0,0 +1,108 @@ +package org.wise.portal.service.chatbot.impl; + +import java.sql.Timestamp; +import java.time.Instant; +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.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; + +/** + * Implementation of ChatbotService for managing chatbot conversations + * + * @author Hiroki Terashima + */ +@Service +public class ChatbotServiceImpl implements ChatbotService { + + @PersistenceContext + private EntityManager entityManager; + + @Autowired + private ChatDao chatDao; + + @Override + @Transactional(readOnly = true) + public List getAllChats(Run run, Workgroup workgroup) { + return chatDao.getChatsByRunAndWorkgroup(run, workgroup); + } + + @Override + @Transactional + 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(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()); + } + 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); + } + }); + } + + chatDao.save(existingChat); + return existingChat; + } + + @Override + @Transactional + 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); + } + + 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"); + } + } +} 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..8c8876bf8 --- /dev/null +++ b/src/main/java/org/wise/vle/domain/chatbot/Chat.java @@ -0,0 +1,110 @@ +/** + * 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; + + @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<>(); + + @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..27066b77e --- /dev/null +++ b/src/main/java/org/wise/vle/domain/chatbot/ChatMessage.java @@ -0,0 +1,80 @@ +/** + * 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; + + @Column(name = "nodeId", length = 30, nullable = false) + private String nodeId; + + @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/application-dockerdev-sample.properties b/src/main/resources/application-dockerdev-sample.properties index 6c97c6c28..d3b2b0a73 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.runtime.endpoint= + diff --git a/src/main/resources/application_sample.properties b/src/main/resources/application_sample.properties index e0b196554..cba99ec23 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.runtime.endpoint= diff --git a/src/main/resources/wise_db_init.sql b/src/main/resources/wise_db_init.sql index febe460de..3e5cf652d 100644 --- a/src/main/resources/wise_db_init.sql +++ b/src/main/resources/wise_db_init.sql @@ -90,6 +90,33 @@ 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, + isDeleted bit not null default 0, + 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, + nodeId varchar(30) not null, + 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,