From 0c3e0fe198c06eadbf99bc8471b00cb94ffd99ac Mon Sep 17 00:00:00 2001 From: kanguk Date: Fri, 18 Oct 2024 19:03:10 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20STT=20=EC=B4=88=EA=B8=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 파일 업로드 -> STT 실행의 기본 기능을 구현한다. --- .../SpeechRecognitionController.java | 24 ++++++++ .../stt/service/ClovaSpeechServiceImpl.java | 56 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java create mode 100644 src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java diff --git a/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java b/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java new file mode 100644 index 00000000..e10f6b0c --- /dev/null +++ b/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java @@ -0,0 +1,24 @@ +package com.splanet.splanet.stt.controller; + +import com.splanet.splanet.stt.service.ClovaSpeechService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/stt") +public class SpeechRecognitionController { + + private final ClovaSpeechService clovaSpeechService; + + public SpeechRecognitionController(ClovaSpeechService clovaSpeechService) { + this.clovaSpeechService = clovaSpeechService; + } + + @PostMapping(consumes = "multipart/form-data") + public String recognizeSpeech(@RequestParam("file") MultipartFile file) { + return clovaSpeechService.recognize(file); + } +} diff --git a/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java new file mode 100644 index 00000000..294d7a92 --- /dev/null +++ b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java @@ -0,0 +1,56 @@ +package com.splanet.splanet.stt.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.splanet.splanet.core.exception.BusinessException; +import com.splanet.splanet.core.exception.ErrorCode; +import com.splanet.splanet.core.properties.ClovaProperties; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.io.entity.ByteArrayEntity; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +public class ClovaSpeechServiceImpl implements ClovaSpeechService { + + private final ClovaProperties clovaProperties; + + public ClovaSpeechServiceImpl(ClovaProperties clovaProperties) { + this.clovaProperties = clovaProperties; + } + + @Override + public String recognize(MultipartFile file) { + String apiURL = clovaProperties.getUrl() + "?lang=" + clovaProperties.getLanguage(); // 언어 설정 반영 + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + byte[] audioBytes = file.getBytes(); + + HttpPost httpPost = new HttpPost(apiURL); + httpPost.addHeader("Content-Type", "application/octet-stream"); + httpPost.addHeader("X-NCP-APIGW-API-KEY-ID", clovaProperties.getClientId()); + httpPost.addHeader("X-NCP-APIGW-API-KEY", clovaProperties.getClientSecret()); + + ByteArrayEntity byteArrayEntity = new ByteArrayEntity(audioBytes, ContentType.APPLICATION_OCTET_STREAM); + httpPost.setEntity(byteArrayEntity); + + return httpClient.execute(httpPost, response -> { + int statusCode = response.getCode(); + String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8"); + + if (statusCode == 200) { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode rootNode = objectMapper.readTree(responseBody); + return rootNode.path("text").asText(); + } else { + throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE, "CLOVA Speech API 호출 실패: " + responseBody); + } + }); + } catch (Exception e) { + throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE, "오디오 파일 처리 중 오류 발생: " + e.getMessage()); + } + } +} \ No newline at end of file From cfe94af6165f45aba38c3ebe9ba6d717f108a461 Mon Sep 17 00:00:00 2001 From: kanguk Date: Sat, 19 Oct 2024 02:44:20 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A6=AC=EB=B0=8D=20API=EB=A5=BC=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20STT=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 CLOVA Speech Recognition 에서 CLOVA Speech 실시간 스트리밍 API를 활용하여 실시간 처리를 가능하도록 수정한다. --- .../SpeechRecognitionController.java | 24 -------- .../stt/service/ClovaSpeechServiceImpl.java | 56 ------------------- 2 files changed, 80 deletions(-) delete mode 100644 src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java delete mode 100644 src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java diff --git a/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java b/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java deleted file mode 100644 index e10f6b0c..00000000 --- a/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.splanet.splanet.stt.controller; - -import com.splanet.splanet.stt.service.ClovaSpeechService; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/stt") -public class SpeechRecognitionController { - - private final ClovaSpeechService clovaSpeechService; - - public SpeechRecognitionController(ClovaSpeechService clovaSpeechService) { - this.clovaSpeechService = clovaSpeechService; - } - - @PostMapping(consumes = "multipart/form-data") - public String recognizeSpeech(@RequestParam("file") MultipartFile file) { - return clovaSpeechService.recognize(file); - } -} diff --git a/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java deleted file mode 100644 index 294d7a92..00000000 --- a/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.splanet.splanet.stt.service; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.splanet.splanet.core.exception.BusinessException; -import com.splanet.splanet.core.exception.ErrorCode; -import com.splanet.splanet.core.properties.ClovaProperties; -import org.apache.hc.client5.http.classic.methods.HttpPost; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.io.entity.ByteArrayEntity; -import org.apache.hc.core5.http.io.entity.EntityUtils; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -@Service -public class ClovaSpeechServiceImpl implements ClovaSpeechService { - - private final ClovaProperties clovaProperties; - - public ClovaSpeechServiceImpl(ClovaProperties clovaProperties) { - this.clovaProperties = clovaProperties; - } - - @Override - public String recognize(MultipartFile file) { - String apiURL = clovaProperties.getUrl() + "?lang=" + clovaProperties.getLanguage(); // 언어 설정 반영 - try (CloseableHttpClient httpClient = HttpClients.createDefault()) { - byte[] audioBytes = file.getBytes(); - - HttpPost httpPost = new HttpPost(apiURL); - httpPost.addHeader("Content-Type", "application/octet-stream"); - httpPost.addHeader("X-NCP-APIGW-API-KEY-ID", clovaProperties.getClientId()); - httpPost.addHeader("X-NCP-APIGW-API-KEY", clovaProperties.getClientSecret()); - - ByteArrayEntity byteArrayEntity = new ByteArrayEntity(audioBytes, ContentType.APPLICATION_OCTET_STREAM); - httpPost.setEntity(byteArrayEntity); - - return httpClient.execute(httpPost, response -> { - int statusCode = response.getCode(); - String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8"); - - if (statusCode == 200) { - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode rootNode = objectMapper.readTree(responseBody); - return rootNode.path("text").asText(); - } else { - throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE, "CLOVA Speech API 호출 실패: " + responseBody); - } - }); - } catch (Exception e) { - throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE, "오디오 파일 처리 중 오류 발생: " + e.getMessage()); - } - } -} \ No newline at end of file From c2e8e778920491db5cf022b6b7017ddf7568dd4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Tue, 22 Oct 2024 23:03:43 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20SpringAi=20gpt=20=EC=9A=B0=EC=84=A0?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=EB=A7=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 14 ++++++ .../splanet/splanet/config/OpenAIConfig.java | 21 +++++++++ .../com/splanet/splanet/gpt/ChatResponse.java | 9 ++++ .../java/com/splanet/splanet/gpt/Choice.java | 8 ++++ .../java/com/splanet/splanet/gpt/Message.java | 12 +++++ .../splanet/splanet/gpt/OpenAiChatClient.java | 47 +++++++++++++++++++ .../java/com/splanet/splanet/gpt/Prompt.java | 12 +++++ 7 files changed, 123 insertions(+) create mode 100644 src/main/java/com/splanet/splanet/config/OpenAIConfig.java create mode 100644 src/main/java/com/splanet/splanet/gpt/ChatResponse.java create mode 100644 src/main/java/com/splanet/splanet/gpt/Choice.java create mode 100644 src/main/java/com/splanet/splanet/gpt/Message.java create mode 100644 src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java create mode 100644 src/main/java/com/splanet/splanet/gpt/Prompt.java diff --git a/build.gradle b/build.gradle index f02fed08..bde34fb4 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,12 @@ configurations { repositories { mavenCentral() + maven { + url 'https://repo.spring.io/milestone' + } + maven { + url 'https://repo.spring.io/snapshot' + } } dependencies { @@ -61,6 +67,14 @@ dependencies { testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + //gpt + // implementation 'org.springframework.boot:spring-boot-starter-web' + // implementation 'com.fasterxml.jackson.core:jackson-databind' + // implementation 'org.springframework.boot:spring-boot-starter' + implementation platform("org.springframework.ai:spring-ai-bom:0.8.0") + implementation 'org.springframework.ai:spring-ai-openai' + + } tasks.named('test') { diff --git a/src/main/java/com/splanet/splanet/config/OpenAIConfig.java b/src/main/java/com/splanet/splanet/config/OpenAIConfig.java new file mode 100644 index 00000000..974a3739 --- /dev/null +++ b/src/main/java/com/splanet/splanet/config/OpenAIConfig.java @@ -0,0 +1,21 @@ +package com.splanet.splanet.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class OpenAIConfig { + @Value("${spring.ai.gpt.api-key}") + private String apiKey; + @Bean + public RestTemplate template(){ + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getInterceptors().add((request, body, execution) -> { + request.getHeaders().add("Authorization", "Bearer " + apiKey); + return execution.execute(request, body); + }); + return restTemplate; + } +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/ChatResponse.java b/src/main/java/com/splanet/splanet/gpt/ChatResponse.java new file mode 100644 index 00000000..25d92c71 --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/ChatResponse.java @@ -0,0 +1,9 @@ +package com.splanet.splanet.gpt; + +import lombok.Getter; +import java.util.List; + +@Getter +public class ChatResponse { + private List choices; // Choice 객체 리스트 +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/Choice.java b/src/main/java/com/splanet/splanet/gpt/Choice.java new file mode 100644 index 00000000..be2070a0 --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/Choice.java @@ -0,0 +1,8 @@ +package com.splanet.splanet.gpt; + +import lombok.Getter; + +@Getter +public class Choice { + private Message message; // Message 객체 +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/Message.java b/src/main/java/com/splanet/splanet/gpt/Message.java new file mode 100644 index 00000000..55b59650 --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/Message.java @@ -0,0 +1,12 @@ +package com.splanet.splanet.gpt; + +import lombok.Getter; + +@Getter +public class Message { + private String content; // 메시지 내용 + + public Message(String content) { + this.content = content; + } +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java b/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java new file mode 100644 index 00000000..27da861e --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java @@ -0,0 +1,47 @@ +package com.splanet.splanet.gpt; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; + +import java.util.List; + +@Component +public class OpenAiChatClient { + + private final WebClient webClient; + + @Value("${spring.ai.gpt.api-key}") + private String apiKey; + + public OpenAiChatClient(WebClient.Builder webClientBuilder) { + this.webClient = webClientBuilder.baseUrl("https://api.openai.com/v1").build(); + } + + public String call(String message) { + Message userMessage = new Message(message); // 메시지 객체 생성 + Prompt prompt = new Prompt(List.of(userMessage)); // Message 객체를 리스트로 감싸서 Prompt 생성 + + ChatResponse response = webClient.post() + .uri("/chat/completions") + .header("Authorization", "Bearer " + apiKey) // 프로퍼티에서 가져온 API 키 사용 + .bodyValue(prompt) + .retrieve() + .bodyToMono(ChatResponse.class) + .block(); // 블록하여 응답을 기다림 + + return response != null && response.getChoices() != null && !response.getChoices().isEmpty() + ? response.getChoices().get(0).getMessage().getContent() + : "No response"; + } + + public Flux stream(Prompt prompt) { + return webClient.post() + .uri("/chat/completions") + .header("Authorization", "Bearer " + apiKey) // 프로퍼티에서 가져온 API 키 사용 + .bodyValue(prompt) + .retrieve() + .bodyToFlux(ChatResponse.class); // Stream the response + } +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/Prompt.java b/src/main/java/com/splanet/splanet/gpt/Prompt.java new file mode 100644 index 00000000..27da96c1 --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/Prompt.java @@ -0,0 +1,12 @@ +package com.splanet.splanet.gpt; + +import lombok.Getter; +import lombok.AllArgsConstructor; + +import java.util.List; + +@Getter +@AllArgsConstructor // 모든 필드를 사용하는 생성자를 자동 생성 +public class Prompt { + private List messages; // 메시지 목록 +} \ No newline at end of file From 8fe797676bee4de2bba7a7f5e5f283bf3c3eead6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Wed, 23 Oct 2024 20:43:03 +0900 Subject: [PATCH 4/6] refactor: @Value->@Configurationproperties --- .../splanet/splanet/config/OpenAIConfig.java | 21 ------------------- .../splanet/splanet/gpt/OpenAiChatClient.java | 20 ++++++++---------- .../splanet/splanet/gpt/OpenAiProperties.java | 14 +++++++++++++ 3 files changed, 23 insertions(+), 32 deletions(-) delete mode 100644 src/main/java/com/splanet/splanet/config/OpenAIConfig.java create mode 100644 src/main/java/com/splanet/splanet/gpt/OpenAiProperties.java diff --git a/src/main/java/com/splanet/splanet/config/OpenAIConfig.java b/src/main/java/com/splanet/splanet/config/OpenAIConfig.java deleted file mode 100644 index 974a3739..00000000 --- a/src/main/java/com/splanet/splanet/config/OpenAIConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.splanet.splanet.config; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.client.RestTemplate; - -@Configuration -public class OpenAIConfig { - @Value("${spring.ai.gpt.api-key}") - private String apiKey; - @Bean - public RestTemplate template(){ - RestTemplate restTemplate = new RestTemplate(); - restTemplate.getInterceptors().add((request, body, execution) -> { - request.getHeaders().add("Authorization", "Bearer " + apiKey); - return execution.execute(request, body); - }); - return restTemplate; - } -} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java b/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java index 27da861e..363e30d7 100644 --- a/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java +++ b/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java @@ -1,6 +1,5 @@ package com.splanet.splanet.gpt; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Flux; @@ -11,25 +10,24 @@ public class OpenAiChatClient { private final WebClient webClient; + private final OpenAiProperties openAiProperties; - @Value("${spring.ai.gpt.api-key}") - private String apiKey; - - public OpenAiChatClient(WebClient.Builder webClientBuilder) { + public OpenAiChatClient(WebClient.Builder webClientBuilder, OpenAiProperties openAiProperties) { this.webClient = webClientBuilder.baseUrl("https://api.openai.com/v1").build(); + this.openAiProperties = openAiProperties; } public String call(String message) { - Message userMessage = new Message(message); // 메시지 객체 생성 - Prompt prompt = new Prompt(List.of(userMessage)); // Message 객체를 리스트로 감싸서 Prompt 생성 + Message userMessage = new Message(message); + Prompt prompt = new Prompt(List.of(userMessage)); ChatResponse response = webClient.post() .uri("/chat/completions") - .header("Authorization", "Bearer " + apiKey) // 프로퍼티에서 가져온 API 키 사용 + .header("Authorization", "Bearer " + openAiProperties.getApiKey()) .bodyValue(prompt) .retrieve() .bodyToMono(ChatResponse.class) - .block(); // 블록하여 응답을 기다림 + .block(); return response != null && response.getChoices() != null && !response.getChoices().isEmpty() ? response.getChoices().get(0).getMessage().getContent() @@ -39,9 +37,9 @@ public String call(String message) { public Flux stream(Prompt prompt) { return webClient.post() .uri("/chat/completions") - .header("Authorization", "Bearer " + apiKey) // 프로퍼티에서 가져온 API 키 사용 + .header("Authorization", "Bearer " + openAiProperties.getApiKey()) .bodyValue(prompt) .retrieve() - .bodyToFlux(ChatResponse.class); // Stream the response + .bodyToFlux(ChatResponse.class); } } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/OpenAiProperties.java b/src/main/java/com/splanet/splanet/gpt/OpenAiProperties.java new file mode 100644 index 00000000..282fa863 --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/OpenAiProperties.java @@ -0,0 +1,14 @@ +package com.splanet.splanet.gpt; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Getter +@Setter +@ConfigurationProperties(prefix = "gpt-api-key") +@Configuration +public class OpenAiProperties { + private String apiKey; +} \ No newline at end of file From 0d514674dd87d7d1823e72f90b0bd18d5b82ff85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Fri, 25 Oct 2024 01:14:17 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=EC=8A=A4=EC=BC=80=EC=A4=84=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/splanet/splanet/gpt/ChatResponse.java | 9 ----- .../java/com/splanet/splanet/gpt/Choice.java | 8 ---- .../java/com/splanet/splanet/gpt/Message.java | 9 ++--- .../splanet/splanet/gpt/OpenAiChatClient.java | 36 ++++++++++++------ .../com/splanet/splanet/gpt/RequestBody.java | 13 +++++++ .../splanet/gpt/SchedulePromptGenerator.java | 38 +++++++++++++++++++ .../splanet/splanet/gpt/ScheduleRequest.java | 19 ++++++++++ .../splanet/splanet/gpt/ScheduleResponse.java | 33 ++++++++++++++++ 8 files changed, 131 insertions(+), 34 deletions(-) delete mode 100644 src/main/java/com/splanet/splanet/gpt/ChatResponse.java delete mode 100644 src/main/java/com/splanet/splanet/gpt/Choice.java create mode 100644 src/main/java/com/splanet/splanet/gpt/RequestBody.java create mode 100644 src/main/java/com/splanet/splanet/gpt/SchedulePromptGenerator.java create mode 100644 src/main/java/com/splanet/splanet/gpt/ScheduleRequest.java create mode 100644 src/main/java/com/splanet/splanet/gpt/ScheduleResponse.java diff --git a/src/main/java/com/splanet/splanet/gpt/ChatResponse.java b/src/main/java/com/splanet/splanet/gpt/ChatResponse.java deleted file mode 100644 index 25d92c71..00000000 --- a/src/main/java/com/splanet/splanet/gpt/ChatResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.splanet.splanet.gpt; - -import lombok.Getter; -import java.util.List; - -@Getter -public class ChatResponse { - private List choices; // Choice 객체 리스트 -} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/Choice.java b/src/main/java/com/splanet/splanet/gpt/Choice.java deleted file mode 100644 index be2070a0..00000000 --- a/src/main/java/com/splanet/splanet/gpt/Choice.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.splanet.splanet.gpt; - -import lombok.Getter; - -@Getter -public class Choice { - private Message message; // Message 객체 -} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/Message.java b/src/main/java/com/splanet/splanet/gpt/Message.java index 55b59650..c64ff08c 100644 --- a/src/main/java/com/splanet/splanet/gpt/Message.java +++ b/src/main/java/com/splanet/splanet/gpt/Message.java @@ -1,12 +1,11 @@ package com.splanet.splanet.gpt; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter -public class Message { +@AllArgsConstructor +class Message { + private String role; // 메시지의 역할 (user, assistant 등) private String content; // 메시지 내용 - - public Message(String content) { - this.content = content; - } } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java b/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java index 363e30d7..e1249a10 100644 --- a/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java +++ b/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java @@ -1,5 +1,7 @@ package com.splanet.splanet.gpt; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Flux; @@ -11,35 +13,45 @@ public class OpenAiChatClient { private final WebClient webClient; private final OpenAiProperties openAiProperties; + private final SchedulePromptGenerator promptGenerator; - public OpenAiChatClient(WebClient.Builder webClientBuilder, OpenAiProperties openAiProperties) { + public OpenAiChatClient(WebClient.Builder webClientBuilder, OpenAiProperties openAiProperties, SchedulePromptGenerator promptGenerator) { this.webClient = webClientBuilder.baseUrl("https://api.openai.com/v1").build(); this.openAiProperties = openAiProperties; + this.promptGenerator = new SchedulePromptGenerator(); } - public String call(String message) { - Message userMessage = new Message(message); - Prompt prompt = new Prompt(List.of(userMessage)); + // 스케줄 생성 요청 처리 메소드 + public ScheduleResponse createSchedule(ScheduleRequest scheduleRequest) throws JsonProcessingException { + String jsonResponse = call(scheduleRequest); + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(jsonResponse, ScheduleResponse.class); + } + + private String call(ScheduleRequest scheduleRequest) throws JsonProcessingException { + String jsonRequest = new ObjectMapper().writeValueAsString(new RequestBody("gpt-4o-mini", List.of( + new Message("user", promptGenerator.generateSchedulePrompt(scheduleRequest)) + ))); - ChatResponse response = webClient.post() + // OpenAI API 호출 + String responseJson = webClient.post() .uri("/chat/completions") .header("Authorization", "Bearer " + openAiProperties.getApiKey()) - .bodyValue(prompt) + .bodyValue(jsonRequest) .retrieve() - .bodyToMono(ChatResponse.class) + .bodyToMono(String.class) .block(); - return response != null && response.getChoices() != null && !response.getChoices().isEmpty() - ? response.getChoices().get(0).getMessage().getContent() - : "No response"; + return responseJson; } - public Flux stream(Prompt prompt) { + // 스트리밍 메소드 + public Flux stream(Prompt prompt) { return webClient.post() .uri("/chat/completions") .header("Authorization", "Bearer " + openAiProperties.getApiKey()) .bodyValue(prompt) .retrieve() - .bodyToFlux(ChatResponse.class); + .bodyToFlux(ScheduleResponse.class); } } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/RequestBody.java b/src/main/java/com/splanet/splanet/gpt/RequestBody.java new file mode 100644 index 00000000..35983b26 --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/RequestBody.java @@ -0,0 +1,13 @@ +package com.splanet.splanet.gpt; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class RequestBody { + private String model; // 모델 이름 + private List messages; // 메시지 리스트 +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/SchedulePromptGenerator.java b/src/main/java/com/splanet/splanet/gpt/SchedulePromptGenerator.java new file mode 100644 index 00000000..40e09d7e --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/SchedulePromptGenerator.java @@ -0,0 +1,38 @@ +package com.splanet.splanet.gpt; + +import org.springframework.stereotype.Component; + +@Component +public class SchedulePromptGenerator { + + public String generateSchedulePrompt(ScheduleRequest request) { + StringBuilder prompt = new StringBuilder(); + + prompt.append("splanet은 사용자가 입력한 스케줄 정보에 맞춰 맞춤형 플래너를 제공하는 서비스입니다. 사용자가 음성으로 입력한 정보를 분석하여 최적의 스케줄을 제시해야 합니다.\n\n") + .append("사용자가 다음과 같은 정보를 입력했습니다. 이 정보를 바탕으로 3개의 서로 다른 스케줄을 추천해 주세요. 각 스케줄은 고유한 컨셉을 가져야 하며, 하루 24시간을 30분 단위로 쪼개고, 각 업무의 시작 시간과 종료 시간을 포함해야 합니다.\n\n") + .append("요청 정보:\n") + .append("- 스케줄 기간: \"").append(request.getSchedulePeriod()).append("\"\n") + .append("- 업무 목록: ").append(request.getTaskList()).append("\n") + .append("- 업무 소요 시간: ").append(request.getTaskDurations()).append("\n") + .append("- 우선순위: ").append(request.getPriority()).append("\n") + .append("- 스케줄 컨셉: \"").append(request.getScheduleConcepts().get(0)).append("\", \"") + .append(request.getScheduleConcepts().get(1)).append("\", \"") + .append(request.getScheduleConcepts().get(2)).append("\"\n") // 각 컨셉을 따로 넣도록 + .append("- 하루를 30분 단위로 쪼갠 시간 목록: ").append(request.getTimeSlots()).append("\n") + .append("위 정보를 바탕으로 최적의 플래너 스케줄을 추천할 때 다음 사항을 준수해주세요:\n") + .append("1. 요청된 모든 일정을 사용해야 합니다.\n") + .append("2. 각 스케줄은 고유한 컨셉을 가져야 하며, 3개의 서로 다른 스케줄을 추천합니다.\n") + .append("3. 추천 예시: 사용자가 선택할 수 있도록 3개의 추천 스케줄을 제공합니다.\n") + .append("4. 응답 형식: 답변은 JSON 형식으로만 제공해야 하며, 다음과 같은 구조를 따라야 합니다:\n") + .append(" { \"schedules\": [{ \"concept\": \"스케줄 컨셉\", \"schedule\": [{ \"date\": \"MM-DD\", \"tasks\": [{ \"task\": \"업무명\", \"duration\": \"소요시간\", \"priority\": \"우선순위\", \"startTime\": \"시작시간\", \"endTime\": \"종료시간\" }] }] }] }] }\n") + .append("5. 형식 규칙:\n") + .append(" - 날짜는 MM-DD 형식으로, 시간은 24시간(30분 단위) 형식이어야 합니다.\n") + .append(" - 입력받은 업무를 지정된 일정 안에 모두 포함시켜야 합니다.\n") + .append(" - 우선순위는 고유한 정수 값이어야 합니다.\n") + .append(" - 업무 시간은 주어진 스케줄 기간 안에만 분할하여 채울 수 있습니다.\n") + .append(" - 입력받은 날짜 각각의 일정을 구현해야 합니다.\n") + .append("6. 제외할 내용: 일정 JSON 외에는 다른 내용이 포함되지 않아야 하며, 스케줄링과 관련 없는 질문에는 \"이와 관련된 질문에는 답변할 수 없습니다.\"라고 응답해야 합니다.\n"); + + return prompt.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/ScheduleRequest.java b/src/main/java/com/splanet/splanet/gpt/ScheduleRequest.java new file mode 100644 index 00000000..e6f254e0 --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/ScheduleRequest.java @@ -0,0 +1,19 @@ +package com.splanet.splanet.gpt; + +import lombok.*; + +import java.util.List; +import java.util.Map; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ScheduleRequest { + private String schedulePeriod; // 스케줄 기간 + private List taskList; // 업무 목록 + private Map taskDurations; // 업무 소요 시간 (업무명: 소요시간) + private Map priority; // 업무 우선순위 (업무명: 우선순위) + private List scheduleConcepts; // 스케줄 컨셉 목록 + private List timeSlots; // 30분 단위 시간 목록 +} diff --git a/src/main/java/com/splanet/splanet/gpt/ScheduleResponse.java b/src/main/java/com/splanet/splanet/gpt/ScheduleResponse.java new file mode 100644 index 00000000..86b4dada --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/ScheduleResponse.java @@ -0,0 +1,33 @@ +package com.splanet.splanet.gpt; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class ScheduleResponse { + private List schedules; // 스케줄 리스트 + + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class Schedule { + private String concept; // 스케줄 컨셉 + private List tasks; // 업무 리스트 + + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class Task { + private String task; // 업무명 + private String duration; // 소요 시간 + private int priority; // 우선순위 + private String startTime; // 시작 시간 + private String endTime; // 종료 시간 + } + } +} \ No newline at end of file From 0c16622e4fd8f6aa2bc503d1f8501dd5325bd056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Fri, 25 Oct 2024 01:46:42 +0900 Subject: [PATCH 6/6] =?UTF-8?q?refactor:=20=EC=97=AC=EB=9F=AC=20=EC=8A=A4?= =?UTF-8?q?=EC=BC=80=EC=A4=84=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../splanet/gpt/SchedulePromptGenerator.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/splanet/splanet/gpt/SchedulePromptGenerator.java b/src/main/java/com/splanet/splanet/gpt/SchedulePromptGenerator.java index 40e09d7e..f3266881 100644 --- a/src/main/java/com/splanet/splanet/gpt/SchedulePromptGenerator.java +++ b/src/main/java/com/splanet/splanet/gpt/SchedulePromptGenerator.java @@ -8,21 +8,19 @@ public class SchedulePromptGenerator { public String generateSchedulePrompt(ScheduleRequest request) { StringBuilder prompt = new StringBuilder(); - prompt.append("splanet은 사용자가 입력한 스케줄 정보에 맞춰 맞춤형 플래너를 제공하는 서비스입니다. 사용자가 음성으로 입력한 정보를 분석하여 최적의 스케줄을 제시해야 합니다.\n\n") - .append("사용자가 다음과 같은 정보를 입력했습니다. 이 정보를 바탕으로 3개의 서로 다른 스케줄을 추천해 주세요. 각 스케줄은 고유한 컨셉을 가져야 하며, 하루 24시간을 30분 단위로 쪼개고, 각 업무의 시작 시간과 종료 시간을 포함해야 합니다.\n\n") + prompt.append("splanet은 사용자가 입력한 스케줄 정보를 바탕으로 맞춤형 플래너를 제공하는 서비스입니다. 사용자가 음성으로 입력한 정보를 분석하여 최적의 스케줄을 제시해야 합니다.\n\n") + .append("사용자가 다음과 같은 정보를 입력했습니다. 이 정보를 바탕으로 요청된 컨셉에 따라 스케줄을 추천해 주세요. 각 스케줄은 하루 24시간을 30분 단위로 쪼개고, 각 업무의 시작 시간과 종료 시간을 포함해야 합니다.\n\n") .append("요청 정보:\n") - .append("- 스케줄 기간: \"").append(request.getSchedulePeriod()).append("\"\n") + .append("- 스케줄 기간: \"10월 1일부터 10월 2일까지\"\n") .append("- 업무 목록: ").append(request.getTaskList()).append("\n") .append("- 업무 소요 시간: ").append(request.getTaskDurations()).append("\n") .append("- 우선순위: ").append(request.getPriority()).append("\n") - .append("- 스케줄 컨셉: \"").append(request.getScheduleConcepts().get(0)).append("\", \"") - .append(request.getScheduleConcepts().get(1)).append("\", \"") - .append(request.getScheduleConcepts().get(2)).append("\"\n") // 각 컨셉을 따로 넣도록 + .append("- 스케줄 컨셉: \"널널한 스케줄\", \"빡빡한 스케줄\"\n") .append("- 하루를 30분 단위로 쪼갠 시간 목록: ").append(request.getTimeSlots()).append("\n") - .append("위 정보를 바탕으로 최적의 플래너 스케줄을 추천할 때 다음 사항을 준수해주세요:\n") + .append("위 정보를 바탕으로 다음과 같은 조건을 준수하여 스케줄을 추천해주세요:\n") .append("1. 요청된 모든 일정을 사용해야 합니다.\n") - .append("2. 각 스케줄은 고유한 컨셉을 가져야 하며, 3개의 서로 다른 스케줄을 추천합니다.\n") - .append("3. 추천 예시: 사용자가 선택할 수 있도록 3개의 추천 스케줄을 제공합니다.\n") + .append("2. 각 스케줄은 요청된 두 개의 서로 다른 컨셉(사용자가 입력한다. 예시로는 널널한 스케줄, 빡빡한 스케줄이 있다.)을 따릅니다.\n") + .append("3. 추천 예시: 사용자가 선택할 수 있도록 두 개의 추천 스케줄을 제공합니다.\n") .append("4. 응답 형식: 답변은 JSON 형식으로만 제공해야 하며, 다음과 같은 구조를 따라야 합니다:\n") .append(" { \"schedules\": [{ \"concept\": \"스케줄 컨셉\", \"schedule\": [{ \"date\": \"MM-DD\", \"tasks\": [{ \"task\": \"업무명\", \"duration\": \"소요시간\", \"priority\": \"우선순위\", \"startTime\": \"시작시간\", \"endTime\": \"종료시간\" }] }] }] }] }\n") .append("5. 형식 규칙:\n") @@ -31,7 +29,8 @@ public String generateSchedulePrompt(ScheduleRequest request) { .append(" - 우선순위는 고유한 정수 값이어야 합니다.\n") .append(" - 업무 시간은 주어진 스케줄 기간 안에만 분할하여 채울 수 있습니다.\n") .append(" - 입력받은 날짜 각각의 일정을 구현해야 합니다.\n") - .append("6. 제외할 내용: 일정 JSON 외에는 다른 내용이 포함되지 않아야 하며, 스케줄링과 관련 없는 질문에는 \"이와 관련된 질문에는 답변할 수 없습니다.\"라고 응답해야 합니다.\n"); + .append("6. 예시: 결과값으로 각 날짜마다 널널한 스케줄과 빡빡한 스케줄 컨셉을 입력받으면, 스케줄1(빡빡한 스케줄): 10월1일+10월2일, 스케줄2(널널한 스케줄): 10월1일+10월2일 총 4개의 스케줄을 제시해야 합니다. 오직 스케줄 데이터 정보만 json으로 출력한다.\n") + .append("7. 제외할 내용: 일정 JSON 외에는 다른 내용이 포함되지 않아야 하며, 스케줄링과 관련 없는 질문에는 \"이와 관련된 질문에는 답변할 수 없습니다.\"라고 응답해야 합니다.\n"); return prompt.toString(); }