diff --git a/src/main/java/com/project/oof/api/controller/ChatGPTController.java b/src/main/java/com/project/oof/api/controller/ChatGPTController.java new file mode 100644 index 0000000..072749a --- /dev/null +++ b/src/main/java/com/project/oof/api/controller/ChatGPTController.java @@ -0,0 +1,83 @@ +package com.project.oof.api.controller; + +import com.project.oof.dto.ChatCompletionDto; +import com.project.oof.dto.CompletionDto; +import com.project.oof.service.ChatGPTService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +/** + * ChatGPT API + * + * @author : lee + * @fileName : ChatGPTController + * @since : 12/29/23 + */ +@Slf4j +@RestController +@RequestMapping(value = "/api/v1/chatGpt") +public class ChatGPTController { + + private final ChatGPTService chatGPTService; + + public ChatGPTController(ChatGPTService chatGPTService) { + this.chatGPTService = chatGPTService; + } + + /** + * [API] ChatGPT 모델 리스트를 조회합니다. + */ + @GetMapping("/modelList") + public ResponseEntity>> selectModelList() { + List> result = chatGPTService.modelList(); + return new ResponseEntity<>(result, HttpStatus.OK); + } + + /** + * [API] ChatGPT 유효한 모델인지 조회합니다. + * + * @param modelName + * @return + */ + @GetMapping("/model") + public ResponseEntity> isValidModel(@RequestParam(name = "modelName") String modelName) { + Map result = chatGPTService.isValidModel(modelName); + return new ResponseEntity<>(result, HttpStatus.OK); + } + + /** + * [API] Legacy ChatGPT 프롬프트 명령을 수행합니다. : gpt-3.5-turbo-instruct, babbage-002, davinci-002 + * + * @param completionDto {} + * @return ResponseEntity> + */ + @PostMapping("/legacyPrompt") + public ResponseEntity> selectLegacyPrompt(@RequestBody CompletionDto completionDto) { + log.debug("param :: " + completionDto.toString()); + Map result = chatGPTService.legacyPrompt(completionDto); + return new ResponseEntity<>(result, HttpStatus.OK); + } + + /** + * [API] 최신 ChatGPT 프롬프트 명령어를 수행합니다. : gpt-4, gpt-4 turbo, gpt-3.5-turbo + * + * @param chatCompletionDto + * @return + */ + @PostMapping("/prompt") + public ResponseEntity> selectPrompt(@RequestBody ChatCompletionDto chatCompletionDto) { + log.debug("param :: " + chatCompletionDto.toString()); + Map result = chatGPTService.prompt(chatCompletionDto); + return new ResponseEntity<>(result, HttpStatus.OK); + } +} \ No newline at end of file diff --git a/src/main/java/com/project/oof/config/ChatGPTConfig.java b/src/main/java/com/project/oof/config/ChatGPTConfig.java new file mode 100644 index 0000000..78efd7d --- /dev/null +++ b/src/main/java/com/project/oof/config/ChatGPTConfig.java @@ -0,0 +1,28 @@ +package com.project.oof.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class ChatGPTConfig { + + @Value("${openai.secret-key}") + private String secretKey; + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + + @Bean + public HttpHeaders httpHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(secretKey); + headers.setContentType(MediaType.APPLICATION_JSON); + return headers; + } +} diff --git a/src/main/java/com/project/oof/dto/ChatCompletionDto.java b/src/main/java/com/project/oof/dto/ChatCompletionDto.java new file mode 100644 index 0000000..1ee0c5f --- /dev/null +++ b/src/main/java/com/project/oof/dto/ChatCompletionDto.java @@ -0,0 +1,30 @@ +package com.project.oof.dto; + +import com.project.oof.dto.request.ChatRequestMsgDto; +import lombok.*; + +import java.util.List; + +/** + * 새로운 모델에 대한 요청 객체를 관리합니다. : gpt-4, gpt-4 turbo, gpt-3.5-turbo + * + * @author : lee + * @fileName : ChatCompletionDto + * @since : 1/18/24 + */ +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ChatCompletionDto { + + // 사용할 모델 + private String model; + + private List messages; + + @Builder + public ChatCompletionDto(String model, List messages) { + this.model = model; + this.messages = messages; + } +} \ No newline at end of file diff --git a/src/main/java/com/project/oof/dto/CompletionDto.java b/src/main/java/com/project/oof/dto/CompletionDto.java new file mode 100644 index 0000000..687bc1a --- /dev/null +++ b/src/main/java/com/project/oof/dto/CompletionDto.java @@ -0,0 +1,33 @@ +package com.project.oof.dto; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CompletionDto { + + // 사용할 모델 + private String model; + + // 사용할 프롬프트 명령어 + private String prompt; + + // 프롬프트의 다양성을 조절할 명령어(default : 1) + private float temperature = 1; + + // 최대 사용할 토큰(default : 16) + private int max_tokens = 16; + + @Builder + public CompletionDto(String model, String prompt, float temperature, int max_tokens) { + this.model = model; + this.prompt = prompt; + this.temperature = temperature; + this.max_tokens = max_tokens; + } +} \ No newline at end of file diff --git a/src/main/java/com/project/oof/dto/request/ChatRequestMsgDto.java b/src/main/java/com/project/oof/dto/request/ChatRequestMsgDto.java new file mode 100644 index 0000000..4dd0094 --- /dev/null +++ b/src/main/java/com/project/oof/dto/request/ChatRequestMsgDto.java @@ -0,0 +1,24 @@ +package com.project.oof.dto.request; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) + +public class ChatRequestMsgDto { + + private String role; + + private String content; + + @Builder + public ChatRequestMsgDto(String role, String content) { + this.role = role; + this.content = content; + } +} \ No newline at end of file diff --git a/src/main/java/com/project/oof/service/ChatGPTService.java b/src/main/java/com/project/oof/service/ChatGPTService.java new file mode 100644 index 0000000..0c1d5a2 --- /dev/null +++ b/src/main/java/com/project/oof/service/ChatGPTService.java @@ -0,0 +1,28 @@ +package com.project.oof.service; + +import com.project.oof.dto.ChatCompletionDto; +import com.project.oof.dto.CompletionDto; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; + +/** + * ChatGPT 서비스 인터페이스 + * + * @author : lee + * @fileName : ChatGPTService + * @since : 12/29/23 + */ + +@Service +public interface ChatGPTService { + + List> modelList(); + + Map isValidModel(String modelName); + + Map legacyPrompt(CompletionDto completionDto); + + Map prompt(ChatCompletionDto chatCompletionDto); +} diff --git a/src/main/java/com/project/oof/service/ChatGPTServiceImpl.java b/src/main/java/com/project/oof/service/ChatGPTServiceImpl.java new file mode 100644 index 0000000..fb5f00a --- /dev/null +++ b/src/main/java/com/project/oof/service/ChatGPTServiceImpl.java @@ -0,0 +1,181 @@ +package com.project.oof.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.project.oof.config.ChatGPTConfig; +import com.project.oof.dto.ChatCompletionDto; +import com.project.oof.dto.CompletionDto; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +public class ChatGPTServiceImpl implements ChatGPTService { + + private final ChatGPTConfig chatGPTConfig; + + public ChatGPTServiceImpl(ChatGPTConfig chatGPTConfig) { + this.chatGPTConfig = chatGPTConfig; + } + + @Value("${openai.url.model}") + private String modelUrl; + + @Value("${openai.url.model-list}") + private String modelListUrl; + + @Value("${openai.url.prompt}") + private String promptUrl; + + @Value("${openai.url.legacy-prompt}") + private String legacyPromptUrl; + + /** + * 사용 가능한 모델 리스트를 조회하는 비즈니스 로직 + * + * @return List> + */ + @Override + public List> modelList() { + log.debug("[+] 모델 리스트를 조회합니다."); + List> resultList = null; + + // [STEP1] 토큰 정보가 포함된 Header를 가져옵니다. + HttpHeaders headers = chatGPTConfig.httpHeaders(); + + // [STEP2] 통신을 위한 RestTemplate을 구성합니다. + ResponseEntity response = chatGPTConfig + .restTemplate() + .exchange(modelUrl, HttpMethod.GET, new HttpEntity<>(headers), String.class); + try { + // [STEP3] Jackson을 기반으로 응답값을 가져옵니다. + ObjectMapper om = new ObjectMapper(); + Map data = om.readValue(response.getBody(), new TypeReference<>() { + }); + + // [STEP4] 응답 값을 결과값에 넣고 출력을 해봅니다. + resultList = (List>) data.get("data"); + for (Map object : resultList) { + log.debug("ID: " + object.get("id")); + log.debug("Object: " + object.get("object")); + log.debug("Created: " + object.get("created")); + log.debug("Owned By: " + object.get("owned_by")); + } + } catch (JsonMappingException e) { + log.debug("JsonMappingException :: " + e.getMessage()); + } catch (JsonProcessingException e) { + log.debug("JsonProcessingException :: " + e.getMessage()); + } catch (RuntimeException e) { + log.debug("RuntimeException :: " + e.getMessage()); + } + return resultList; + } + + /** + * 모델이 유효한지 확인하는 비즈니스 로직 + * + * @param modelName {} + * @return Map + */ + @Override + public Map isValidModel(String modelName) { + log.debug("[+] 모델이 유효한지 조회합니다. 모델 : " + modelName); + Map result = new HashMap<>(); + + // [STEP1] 토큰 정보가 포함된 Header를 가져옵니다. + HttpHeaders headers = chatGPTConfig.httpHeaders(); + + // [STEP2] 통신을 위한 RestTemplate을 구성합니다. + ResponseEntity response = chatGPTConfig + .restTemplate() + .exchange(modelListUrl + "/" + modelName, HttpMethod.GET, new HttpEntity<>(headers), String.class); + try { + // [STEP3] Jackson을 기반으로 응답값을 가져옵니다. + ObjectMapper om = new ObjectMapper(); + result = om.readValue(response.getBody(), new TypeReference<>() { + }); + } catch (JsonProcessingException e) { + log.debug("JsonMappingException :: " + e.getMessage()); + } catch (RuntimeException e) { + log.debug("RuntimeException :: " + e.getMessage()); + } + return result; + } + + /** + * ChatGTP 프롬프트 검색 + * + * @param completionDto completionDto + * @return Map + */ + @Override + public Map legacyPrompt(CompletionDto completionDto) { + log.debug("[+] 레거시 프롬프트를 수행합니다."); + + // [STEP1] 토큰 정보가 포함된 Header를 가져옵니다. + HttpHeaders headers = chatGPTConfig.httpHeaders(); + + // [STEP5] 통신을 위한 RestTemplate을 구성합니다. + HttpEntity requestEntity = new HttpEntity<>(completionDto, headers); + ResponseEntity response = chatGPTConfig + .restTemplate() + .exchange(legacyPromptUrl, HttpMethod.POST, requestEntity, String.class); + + Map resultMap = new HashMap<>(); + try { + ObjectMapper om = new ObjectMapper(); + // [STEP6] String -> HashMap 역직렬화를 구성합니다. + resultMap = om.readValue(response.getBody(), new TypeReference<>() { + }); + } catch (JsonProcessingException e) { + log.debug("JsonMappingException :: " + e.getMessage()); + } catch (RuntimeException e) { + log.debug("RuntimeException :: " + e.getMessage()); + } + return resultMap; + } + + /** + * 신규 모델에 대한 프롬프트 + * + * @param chatCompletionDto {} + * @return chatCompletionDto + */ + @Override + public Map prompt(ChatCompletionDto chatCompletionDto) { + log.debug("[+] 신규 프롬프트를 수행합니다."); + + Map resultMap = new HashMap<>(); + + // [STEP1] 토큰 정보가 포함된 Header를 가져옵니다. + HttpHeaders headers = chatGPTConfig.httpHeaders(); + + // [STEP5] 통신을 위한 RestTemplate을 구성합니다. + HttpEntity requestEntity = new HttpEntity<>(chatCompletionDto, headers); + ResponseEntity response = chatGPTConfig + .restTemplate() + .exchange(promptUrl, HttpMethod.POST, requestEntity, String.class); + try { + // [STEP6] String -> HashMap 역직렬화를 구성합니다. + ObjectMapper om = new ObjectMapper(); + resultMap = om.readValue(response.getBody(), new TypeReference<>() { + }); + } catch (JsonProcessingException e) { + log.debug("JsonMappingException :: " + e.getMessage()); + } catch (RuntimeException e) { + log.debug("RuntimeException :: " + e.getMessage()); + } + return resultMap; + } +}