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/gpt/Message.java b/src/main/java/com/splanet/splanet/gpt/Message.java new file mode 100644 index 00000000..c64ff08c --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/Message.java @@ -0,0 +1,11 @@ +package com.splanet.splanet.gpt; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +class Message { + private String role; // 메시지의 역할 (user, assistant 등) + private String 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..e1249a10 --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java @@ -0,0 +1,57 @@ +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; + +import java.util.List; + +@Component +public class OpenAiChatClient { + + private final WebClient webClient; + private final OpenAiProperties openAiProperties; + private final SchedulePromptGenerator promptGenerator; + + 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 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)) + ))); + + // OpenAI API 호출 + String responseJson = webClient.post() + .uri("/chat/completions") + .header("Authorization", "Bearer " + openAiProperties.getApiKey()) + .bodyValue(jsonRequest) + .retrieve() + .bodyToMono(String.class) + .block(); + + return responseJson; + } + + // 스트리밍 메소드 + public Flux stream(Prompt prompt) { + return webClient.post() + .uri("/chat/completions") + .header("Authorization", "Bearer " + openAiProperties.getApiKey()) + .bodyValue(prompt) + .retrieve() + .bodyToFlux(ScheduleResponse.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 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 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..f3266881 --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/SchedulePromptGenerator.java @@ -0,0 +1,37 @@ +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("사용자가 다음과 같은 정보를 입력했습니다. 이 정보를 바탕으로 요청된 컨셉에 따라 스케줄을 추천해 주세요. 각 스케줄은 하루 24시간을 30분 단위로 쪼개고, 각 업무의 시작 시간과 종료 시간을 포함해야 합니다.\n\n") + .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("- 스케줄 컨셉: \"널널한 스케줄\", \"빡빡한 스케줄\"\n") + .append("- 하루를 30분 단위로 쪼갠 시간 목록: ").append(request.getTimeSlots()).append("\n") + .append("위 정보를 바탕으로 다음과 같은 조건을 준수하여 스케줄을 추천해주세요:\n") + .append("1. 요청된 모든 일정을 사용해야 합니다.\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") + .append(" - 날짜는 MM-DD 형식으로, 시간은 24시간(30분 단위) 형식이어야 합니다.\n") + .append(" - 입력받은 업무를 지정된 일정 안에 모두 포함시켜야 합니다.\n") + .append(" - 우선순위는 고유한 정수 값이어야 합니다.\n") + .append(" - 업무 시간은 주어진 스케줄 기간 안에만 분할하여 채울 수 있습니다.\n") + .append(" - 입력받은 날짜 각각의 일정을 구현해야 합니다.\n") + .append("6. 예시: 결과값으로 각 날짜마다 널널한 스케줄과 빡빡한 스케줄 컨셉을 입력받으면, 스케줄1(빡빡한 스케줄): 10월1일+10월2일, 스케줄2(널널한 스케줄): 10월1일+10월2일 총 4개의 스케줄을 제시해야 합니다. 오직 스케줄 데이터 정보만 json으로 출력한다.\n") + .append("7. 제외할 내용: 일정 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