Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : ai 피드백 추가 #73

Merged
merged 2 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ repositories {
}

ext {
set('springCloudVersion', "2023.0.0")
set('springCloudVersion', "2023.0.3")
}

dependencyManagement {
Expand Down Expand Up @@ -101,6 +101,9 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// feign client
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
}

tasks.named('test') {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/groom/orbit/OrbitApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class OrbitApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.groom.orbit.config.openai;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonValue;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
public class AiFeedbackRequestDto {

private String model = "gpt-4o-mini";

private List<Message> messages;

public AiFeedbackRequestDto(List<Message> messages) {
this.messages = messages;
}

@Getter
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public static class Message {
private Role role;
private List<Content> content;
}

@RequiredArgsConstructor
@Getter
public enum Role {
USER("user"),
SYSTEM("system");
@JsonValue private final String value;
}

@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class Content {
private Type type;

private String text;

public static Content textContent(String text) {
return new Content(Type.TEXT, text);
}
}

@RequiredArgsConstructor
@Getter
public enum Type {
TEXT("text");
@JsonValue private final String value;
}

public static AiFeedbackRequestDto from(
String job,
String academy,
String career,
String qualification,
String experience,
String etc) {
return new AiFeedbackRequestDto(
createMessages(job, academy, career, qualification, experience, etc));
}

private static List<Message> createMessages(
String job,
String academy,
String career,
String qualification,
String experience,
String etc) {
return List.of(
new Message(Role.SYSTEM, createSystemContents(job)),
new Message(Role.USER, createContents(academy, career, qualification, experience, etc)));
}

private static List<Content> createSystemContents(String job) {
return List.of(Content.textContent(Prompt.getSystemPrompt(job)));
}

private static List<Content> createContents(
String academy, String career, String qualification, String experience, String etc) {
return List.of(
Content.textContent(Prompt.getUserPrompt(academy, career, qualification, experience, etc)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.groom.orbit.config.openai;

import java.util.List;

import lombok.Getter;

@Getter
public class AiFeedbackResponseDto {

private List<Choice> choices;

@Getter
public static class Choice {
private Message message;
private Integer index;
}

@Getter
public static class Message {
String role;
String content;
}

public String getAnswer() {
return choices.get(0).message.content;
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/groom/orbit/config/openai/OpenAiClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.groom.orbit.config.openai;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(
name = "open-ai-client",
url = "https://api.openai.com/v1/chat/completions",
configuration = OpenAiClientConfig.class)
public interface OpenAiClient {
@PostMapping
AiFeedbackResponseDto createAiFeedback(@RequestBody AiFeedbackRequestDto requestDto);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.groom.orbit.config.openai;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;

import feign.Logger;
import feign.Logger.Level;
import feign.RequestInterceptor;

public class OpenAiClientConfig {

@Bean
public RequestInterceptor requestInterceptor(@Value("${openai.api-key}") String apiKey) {
return template -> template.header("Authorization", "Bearer " + apiKey);
}

@Bean
public Logger.Level loggerLevel() {
return Level.FULL;
}
}
42 changes: 42 additions & 0 deletions src/main/java/com/groom/orbit/config/openai/Prompt.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.groom.orbit.config.openai;

public class Prompt {
private static final String SYSTEM_PROMPT =
"내 관심 직무 분야는 {job}인데 이에 관한 내 활동사항들이야. 이 항목 대해 피드백을 해줘";
private static final String USER_PROMPT =
"""
{academy}

이건 내 학력이고,

{career}

이건 내 경력이야.

{qualification}

이건 내 자격, 어학, 수상에 관련된 내용들이고

{experience}

이건 내 경험, 활동, 교육에 관한 내용들이야.

{etc}

이건 기타사항이야
""";

public static String getSystemPrompt(String job) {
return SYSTEM_PROMPT.replace("{job}", job);
}

public static String getUserPrompt(
String academy, String career, String qualification, String experience, String etc) {
USER_PROMPT.replace("{academy}", academy);
USER_PROMPT.replace("{career}", career);
USER_PROMPT.replace("{qualification}", qualification);
USER_PROMPT.replace("{experience}", experience);
USER_PROMPT.replace("{etc}", etc);
return USER_PROMPT;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.groom.orbit.member.app.dto.response;

import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.groom.orbit.config.openai.AiFeedbackRequestDto;
import com.groom.orbit.config.openai.AiFeedbackResponseDto;
import com.groom.orbit.config.openai.OpenAiClient;
import com.groom.orbit.job.app.InterestJobService;
import com.groom.orbit.job.app.dto.JobDetailResponseDto;
import com.groom.orbit.member.app.MemberQueryService;
import com.groom.orbit.member.dao.jpa.MemberRepository;
import com.groom.orbit.member.dao.jpa.entity.Member;
import com.groom.orbit.resume.app.ResumeQueryService;
import com.groom.orbit.resume.app.dto.GetResumeResponseDto;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
@Transactional
public class MemberCommandService {

private final MemberRepository memberRepository;
private final InterestJobService interestJobService;
private final ResumeQueryService resumeQueryService;
private final MemberQueryService memberQueryService;
private final OpenAiClient openAiClient;

private AiFeedbackRequestDto createAiFeedbackRequest(Long memberId) {

List<String> jobName =
interestJobService.findJobsByMemberId(memberId).stream()
.map(JobDetailResponseDto::name)
.toList();
String job = String.join(",", jobName);

GetResumeResponseDto getResumeResponseDto = resumeQueryService.getResume(memberId);

List<String> categoryStringList =
resumeQueryService.convertToResumeStrings(getResumeResponseDto);

String academy = categoryStringList.get(0);
String career = categoryStringList.get(1);
String qualification = categoryStringList.get(2);
String experience = categoryStringList.get(3);
String etc = categoryStringList.get(4);

return AiFeedbackRequestDto.from(job, academy, career, qualification, experience, etc);
}

public String createAiFeedbackResponse(Long memberId) {
AiFeedbackResponseDto responseDto =
openAiClient.createAiFeedback(createAiFeedbackRequest(memberId));

Member member = memberQueryService.findMember(memberId);

member.setAiFeedback(responseDto.getAnswer());

memberRepository.save(member);

return responseDto.getAnswer();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.groom.orbit.member.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.groom.orbit.common.annotation.AuthMember;
import com.groom.orbit.common.dto.ResponseDto;
import com.groom.orbit.member.app.dto.response.MemberCommandService;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/member")
public class MemberCommandController {

private final MemberCommandService memberCommandService;

@PostMapping("/ai")
public ResponseDto<String> createAiFeedback(@AuthMember Long memberId) {
return ResponseDto.ok(memberCommandService.createAiFeedbackResponse(memberId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity(name = "member")
@Table(name = "member")
Expand Down Expand Up @@ -49,6 +50,10 @@ public class Member {
@Column(name = "is_profile")
private Boolean isProfile = false;

@Setter
@Column(name = "ai_feedback", length = 50000)
private String aiFeedback = "";

@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
private List<InterestJob> interestJobs = new ArrayList<>();

Expand Down
42 changes: 42 additions & 0 deletions src/main/java/com/groom/orbit/resume/app/ResumeQueryService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.groom.orbit.resume.app;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -35,4 +37,44 @@ public GetResumeResponseDto getResume(Long memberId) {
categorizedResumes.getOrDefault(ResumeCategory.EXPERIENCE, List.of()),
categorizedResumes.getOrDefault(ResumeCategory.ETC, List.of()));
}

public List<String> convertToResumeStrings(GetResumeResponseDto responseDto) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

Map<ResumeCategory, List<ResumeResponseDto>> categorizedResumes =
Map.of(
ResumeCategory.ACADEMY, responseDto.academyList(),
ResumeCategory.CAREER, responseDto.careerList(),
ResumeCategory.QUALIFICATION, responseDto.qualificationList(),
ResumeCategory.EXPERIENCE, responseDto.experienceList(),
ResumeCategory.ETC, responseDto.etcList());

return Arrays.stream(ResumeCategory.values())
.map(
category -> {
List<ResumeResponseDto> resumeList =
categorizedResumes.getOrDefault(category, List.of());

if (resumeList.isEmpty()) {
return String.format("Category: %s\n없음.\n", category);
}

String details =
resumeList.stream()
.map(
dto -> {
String startDate =
dto.startDate() != null ? sdf.format(dto.startDate()) : "N/A";
String endDate =
dto.endDate() != null ? sdf.format(dto.endDate()) : "N/A";
return String.format(
"제목: %s\n내용: %s\n시작일: %s\n마감일: %s",
dto.title(), dto.content(), startDate, endDate);
})
.collect(Collectors.joining("\n\n"));

return String.format("Category: %s\n%s", category, details);
})
.collect(Collectors.toList());
}
}
Loading
Loading