[OpenAI] OpenAI 를 활용한 챗봇 만들기 1 - 채팅하기

류재성's avatar
Jun 30, 2024
[OpenAI] OpenAI 를 활용한 챗봇 만들기 1 - 채팅하기
 

1. 기본 설정

 
build.gradle
💡
의존성 설정
//Spring MVC 및 RESTful 웹 서비스를 만들기 위한 기본 implementation 'org.springframework.boot:spring-boot-starter-web' //Spring Boot 애플리케이션을 테스트하기 위한 기본 의존성 testImplementation 'org.springframework.boot:spring-boot-starter-test' //Spring Data JPA를 사용하여 데이터베이스와 상호작용하기 위한 의존성 implementation 'org.springframework.boot:spring-boot-starter-data-jpa' //WebSocket을 사용하여 실시간 양방향 통신을 구현하기 위한 의존성 implementation group: 'org.springframework.boot', name: 'spring-boot-starter-websocket', version: '3.3.1' // mustache 탬플릿을 위한 의존 implementation 'org.springframework.boot:spring-boot-starter-mustache'
 
application.yml
💡
yml 설정
spring: datasource: initialize: false openai: api: key: ${OPEN_API_KEY}
 
_core/config/WebSocketConfig
💡
웹 소켓 연결 설정
package org.example.chatai._core.config; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; // 설정 클래 @Configuration // STOMP 프로토콜을 사용하여 WebSocket을 통해 메시지를 주고받을 수 있음. @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { // 메세지 라우터 @Override public void configureMessageBroker(MessageBrokerRegistry config) { // /topic 이 붙은 목적지를 구독하는 클라이언트에게 메세지 전달 config.enableSimpleBroker("/topic"); // 클라이언트가 메세지를 보낼때 목적지의 접두사 config.setApplicationDestinationPrefixes("/app"); } // stomp 엔드포인트 등록. 엔드포인트를 통해 WebSocket 연결 시작 @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // /chat-websocket 를 엔드포인트로 설정. 웹소켓 연결 시작 registry.addEndpoint("/chat-websocket").withSockJS(); } }
 
 

2. OpenAI 통신 설정

openAI/OpenAIService
💡
openAI 와 통신
package org.example.chatai.OpenAI; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.List; import java.util.Map; @Service public class OpenAIService { // application.yml 에서 api key 주입받음 @Value("${openai.api.key}") private String apiKey; // 요청 api 주소 private final String apiUrl = "https://api.openai.com/v1/chat/completions"; // RESTful 웹 서비스 사용위해 private final RestTemplate restTemplate = new RestTemplate(); // 질문 받고 응답 public String askOpenAI(String prompt) { // 요청할 모델 지정 Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", "gpt-3.5-turbo"); requestBody.put("messages", List.of( Map.of("role", "user", "content", prompt) )); // 헤더 설정 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("Authorization", "Bearer " + apiKey); // 헤더 바디를 포함한 요청 객체 생성 HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers); // api 요청 후 응답 받음 ResponseEntity<Map> response = restTemplate.postForEntity(apiUrl, entity, Map.class); // 응답된 메세지 반환 if (response.getBody() != null) { List<Map<String, Object>> choices = (List<Map<String, Object>>) response.getBody().get("choices"); if (choices != null && !choices.isEmpty()) { Map<String, Object> message = (Map<String, Object>) choices.get(0).get("message"); return message != null ? (String) message.get("content") : "No response content"; } } return "Error: No response from OpenAI"; } }
 

3. 채팅 설정

 
chat/ChatController
💡
클라이언트로부터 오는 메세지 처리
package org.example.chatai.chat; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.stereotype.Controller; @RequiredArgsConstructor @Controller public class ChatController { private final ChatService chatService; // 클라이언트가 입력한 메세지 서버로 전달 @MessageMapping("/chat.sendMessage") public void sendMessage(@Payload ChatRequest.ChatMessageDTO chatMessage) { chatService.processMessage(chatMessage); } // 클라이언트 정보 저장 @MessageMapping("/chat.addUser") public void addUser(@Payload ChatRequest.ChatMessageDTO chatMessage, SimpMessageHeaderAccessor headerAccessor) { headerAccessor.getSessionAttributes().put("username", chatMessage.getSender()); } }
 
 
chat/ChatService
💡
클라이언트에게 받은 메세지를 openAI 에 전달, 응답받은 정보를 클라이언트에게 전달
package org.example.chatai.chat; import lombok.RequiredArgsConstructor; import org.example.chatai.OpenAI.OpenAIService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; @RequiredArgsConstructor @Service public class ChatService { // STOMP 메시지를 클라이언트로 보내기 위한 템플릿 객체 private final SimpMessagingTemplate messagingTemplate; private final OpenAIService openAIService; // 클라이언트로부터 받은 메시지를 처 public void processMessage(ChatRequest.ChatMessageDTO requestDTO) { // 클라이언트로부터 입력받은 메세지 저장 String userMessage = requestDTO.getContent(); // userMessage 를 openAI에 전달 후 받은 응답을 aiResponse 에 저장 String aiResponse = openAIService.askOpenAI(userMessage); // 응답받은 메세지를 DTO에 담 ChatResponse.ChatMessageDTO aiMessage = new ChatResponse.ChatMessageDTO(aiResponse); // /topic/messages 경로로 전달해 클라이언트가 메세지를 전달받음 messagingTemplate.convertAndSend("/topic/messages", aiMessage); } }
 
chat/ChatRequest
💡
요청 DTO
package org.example.chatai.chat; import lombok.Data; public class ChatRequest { @Data public static class ChatMessageDTO { // 요청 내용 private String content; // 클라이언트 이름 private String sender; } }
 
chat/ChatResponse
💡
응답 DTO
package org.example.chatai.chat; import lombok.Data; public class ChatResponse { @Data public static class ChatMessageDTO { private String content; private String sender; public ChatMessageDTO(String aiResponse) { this.content = aiResponse; // 응답받은 내용 this.sender = "AI"; // 발신자를 AI 로 지정 } } }
 
 

4. 화면

 
HomeController
package org.example.chatai; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class HomeController { @GetMapping("/") public String index() { return "index"; } }
 
 
 
index.mustache 전체 코드
<!DOCTYPE html> <html> <head> <title>AI Chatbot Example</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> <style> .message { padding: 5px; margin: 5px; border-radius: 5px; max-width: 80%; } .user-message { background-color: #DCF8C6; text-align: right; float: right; clear: both; } .ai-message { background-color: #EDEDED; text-align: left; float: left; clear: both; } #chatArea { height: 300px; overflow-y: scroll; border: 1px solid black; padding: 10px; } </style> </head> <body> <div class="container"> <h1>Chatbot Example</h1> <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#chatModal"> Open Chat </button> </div> <!-- Modal --> <div class="modal fade" id="chatModal" tabindex="-1" aria-labelledby="chatModalLabel" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="chatModalLabel">Chat with AI</h5> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">&times;</span> </button> </div> <div class="modal-body"> <div> <label for="username">Username:</label> <input type="text" id="username"/> <button id="connectBtn" class="btn btn-secondary">Connect</button> </div> <br/> <div id="chatArea"></div> <br/> <div> <textarea id="message" rows="3" cols="40"></textarea> <br/> <button id="sendBtn" class="btn btn-primary">Send</button> </div> </div> </div> </div> </div> </body> </html> <script type="text/javascript"> var stompClient = null; function connect() { var socket = new SockJS('/chat-websocket'); stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { console.log('Connected: ' + frame); stompClient.subscribe('/topic/messages', function(response) { showMessage(JSON.parse(response.body)); }); }); } function sendMessage() { var messageContent = $('#message').val(); var username = $('#username').val(); var chatMessage = { content: messageContent, sender: username }; stompClient.send('/app/chat.sendMessage', {}, JSON.stringify(chatMessage)); $('#message').val(''); showUserMessage(chatMessage); // 사용자의 메시지를 바로 표시 } function showMessage(message) { var messageElement = $('<div class="message ai-message"></div>'); messageElement.text(message.sender + ': ' + message.content); $('#chatArea').append(messageElement); $('#chatArea').scrollTop($('#chatArea')[0].scrollHeight); // 스크롤을 아래로 } function showUserMessage(message) { var messageElement = $('<div class="message user-message"></div>'); messageElement.text(message.sender + ': ' + message.content); $('#chatArea').append(messageElement); $('#chatArea').scrollTop($('#chatArea')[0].scrollHeight); // 스크롤을 아래로 } $(function () { $('form').on('submit', function (e) { e.preventDefault(); }); $('#connectBtn').click(function() { connect(); }); $('#sendBtn').click(function() { sendMessage(); }); }); </script>
 
index.mustache 자바스크립트 부분
<script type="text/javascript"> var stompClient = null; // connect 버튼 입력 시 실행, 웹소켓 연결 function connect() { var socket = new SockJS('/chat-websocket'); stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { console.log('Connected: ' + frame); stompClient.subscribe('/topic/messages', function(response) { showMessage(JSON.parse(response.body)); }); }); } // 메세지 전달 function sendMessage() { var messageContent = $('#message').val(); var username = $('#username').val(); var chatMessage = { content: messageContent, sender: username }; stompClient.send('/app/chat.sendMessage', {}, JSON.stringify(chatMessage)); $('#message').val(''); showUserMessage(chatMessage); // 사용자의 메시지를 바로 표시 } // 메세지 표시 function showMessage(message) { var messageElement = $('<div class="message ai-message"></div>'); messageElement.text(message.sender + ': ' + message.content); $('#chatArea').append(messageElement); $('#chatArea').scrollTop($('#chatArea')[0].scrollHeight); // 스크롤을 아래로 } // 클라이언트 정보 표시 function showUserMessage(message) { var messageElement = $('<div class="message user-message"></div>'); messageElement.text(message.sender + ': ' + message.content); $('#chatArea').append(messageElement); $('#chatArea').scrollTop($('#chatArea')[0].scrollHeight); // 스크롤을 아래로 } $(function () { $('form').on('submit', function (e) { e.preventDefault(); }); $('#connectBtn').click(function() { connect(); }); $('#sendBtn').click(function() { sendMessage(); }); }); </script>
 
 
notion image
 
 
Share article

{CODE-RYU};