Skip to content

Commit

Permalink
Merge pull request #242 from 9oormthon-univ/feat/#238
Browse files Browse the repository at this point in the history
feat: 사용자 목표 추천 api 추가
  • Loading branch information
koosco authored Dec 4, 2024
2 parents 8959255 + ad8e684 commit cf31401
Show file tree
Hide file tree
Showing 25 changed files with 300 additions and 170 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.groom.orbit.ai;
package com.groom.orbit.ai.app;

import com.groom.orbit.goal.app.dto.request.CreateGoalRequestDto;
import com.groom.orbit.member.app.dto.response.GetFeedbackResponseDto;
import com.groom.orbit.resume.app.dto.GetResumeResponseDto;

public interface AiService {

GetFeedbackResponseDto getMemberFeedback(String interestJobs, GetResumeResponseDto dto);

CreateGoalRequestDto recommendGoal(Long memberId);
}
62 changes: 0 additions & 62 deletions src/main/java/com/groom/orbit/ai/app/OpenAiService.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package com.groom.orbit.ai;
package com.groom.orbit.ai.app;

import java.util.List;

import com.groom.orbit.ai.app.dto.CreateVectorDto;
import com.groom.orbit.ai.app.dto.UpdateVectorGoalDto;
import com.groom.orbit.ai.app.dto.UpdateVectorQuestDto;
import com.groom.orbit.ai.dao.vector.Vector;

public interface VectorService {

List<Vector> findSimilarVector(Long memberId);

void save(CreateVectorDto dto);

void updateGoal(UpdateVectorGoalDto dto);
Expand Down
105 changes: 105 additions & 0 deletions src/main/java/com/groom/orbit/ai/app/openai/OpenAiService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.groom.orbit.ai.app.openai;

import java.util.List;
import java.util.Map;

import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

import com.groom.orbit.ai.app.AiService;
import com.groom.orbit.ai.app.VectorService;
import com.groom.orbit.ai.dao.vector.Vector;
import com.groom.orbit.goal.app.dto.request.CreateGoalRequestDto;
import com.groom.orbit.member.app.dto.response.GetFeedbackResponseDto;
import com.groom.orbit.resume.app.dto.GetResumeResponseDto;
import com.groom.orbit.resume.app.dto.ResumeResponseDto;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class OpenAiService implements AiService {

private final ChatModel chatModel;
private final VectorService vectorService;

@Value("classpath:/templates/ai-feedback-prompt.txt")
private Resource aiFeedbackPrompt;

@Value("classpath:/templates/goal-recommend-prompt.txt")
private Resource goalRecommendPrompt;

private static final String PARAMETER_NEW_LINE_LIST_DELIMITER = "\n -";
private static final String PARAMETER_LIST_DELIMITER = ",";

public GetFeedbackResponseDto getMemberFeedback(String interestJobs, GetResumeResponseDto dto) {
BeanOutputConverter<GetFeedbackResponseDto> converter =
getConverter(GetFeedbackResponseDto.class);
String format = converter.getFormat();

PromptTemplate promptTemplate = new PromptTemplate(aiFeedbackPrompt);
String response =
callChatModel(
promptTemplate,
Map.of(
"job", interestJobs,
"academy", convertResumeDtoToString(dto.academyList()),
"career", convertResumeDtoToString(dto.careerList()),
"qualification", convertResumeDtoToString(dto.qualificationList()),
"experience", convertResumeDtoToString(dto.experienceList()),
"etc", convertResumeDtoToString(dto.etcList()),
"format", format));

return converter.convert(response);
}

@Override
public CreateGoalRequestDto recommendGoal(Long memberId) {
BeanOutputConverter<CreateGoalRequestDto> converter = getConverter(CreateGoalRequestDto.class);
String format = converter.getFormat();
List<Vector> similarVectors = vectorService.findSimilarVector(memberId);

Vector myVector = similarVectors.getFirst();
List<Vector> othersVector = similarVectors.subList(1, similarVectors.size());

PromptTemplate promptTemplate = new PromptTemplate(goalRecommendPrompt);
String response =
callChatModel(
promptTemplate,
Map.of(
"job", String.join(PARAMETER_LIST_DELIMITER, myVector.interestJobs()),
"myGoal", String.join(PARAMETER_LIST_DELIMITER, myVector.goals()),
"goalList",
String.join(
PARAMETER_LIST_DELIMITER,
othersVector.stream().flatMap(vector -> vector.goals().stream()).toList()),
"format", format));
return converter.convert(response);
}

private <T> BeanOutputConverter<T> getConverter(Class<T> converterClass) {
return new BeanOutputConverter<>(converterClass);
}

private String convertResumeDtoToString(List<ResumeResponseDto> data) {
return String.join(
PARAMETER_NEW_LINE_LIST_DELIMITER, data.stream().map(ResumeResponseDto::title).toList());
}

private String callChatModel(PromptTemplate promptTemplate, Map<String, Object> variables) {
Prompt prompt = promptTemplate.create(variables);
ChatResponse response = chatModel.call(prompt);

log.info("prompt is {}", prompt.getContents());

return response.getResult().getOutput().getContent();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.groom.orbit.ai.app;
package com.groom.orbit.ai.app.pinecone;

import static com.groom.orbit.ai.app.util.PineconeConst.EMBEDDING_MODEL;
import static com.groom.orbit.ai.app.util.PineconeConst.INPUT_TYPE_PARAMETER_KEY;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.groom.orbit.ai.app;
package com.groom.orbit.ai.app.pinecone;

import static com.groom.orbit.ai.app.util.PineconeConst.DEFAULT_MEMBER_NAME;

Expand All @@ -15,31 +15,45 @@
import com.google.protobuf.Struct;
import com.google.protobuf.Struct.Builder;
import com.google.protobuf.Value;
import com.groom.orbit.ai.VectorService;
import com.groom.orbit.ai.app.VectorService;
import com.groom.orbit.ai.app.dto.CreateVectorDto;
import com.groom.orbit.ai.app.dto.UpdateVectorGoalDto;
import com.groom.orbit.ai.app.dto.UpdateVectorQuestDto;
import com.groom.orbit.ai.app.util.PineconeVectorMapper;
import com.groom.orbit.ai.dao.PineconeVectorStore;
import com.groom.orbit.ai.dao.VectorStore;
import com.groom.orbit.ai.dao.pinecone.PineconeVectorStore;
import com.groom.orbit.ai.dao.vector.Vector;
import com.groom.orbit.common.exception.CommonException;
import com.groom.orbit.common.exception.ErrorCode;

@Service
public class PineconeService implements VectorService {

private final PineconeVectorMapper mapper;
private final PineconeVectorStore vectorStore;
private final VectorStore vectorStore;
private final PineconeEmbeddingService embeddingService;
private final PineconeVectorStore pineconeVectorStore;

public PineconeService(
PineconeVectorMapper mapper,
PineconeVectorStore vectorStore,
PineconeEmbeddingService embeddingService,
PineconeVectorStore pineconeVectorStore) {
PineconeEmbeddingService embeddingService) {
this.mapper = mapper;
this.vectorStore = vectorStore;
this.embeddingService = embeddingService;
this.pineconeVectorStore = pineconeVectorStore;
}

@Override
public List<Vector> findSimilarVector(Long memberId) {
Vector vector =
findVector(memberId).orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_VECTOR));

String stringVector = mapper.mapToString(vector);
List<String> inputs = List.of(stringVector);
List<Embedding> embeddedInputs = embeddingService.embed(inputs);
List<Float> embeddedVector = toEmbeddingVector(embeddedInputs);
List<Vector> similarVectors = vectorStore.findSimilar(embeddedVector);

return similarVectors.stream().toList();
}

@Override
Expand Down Expand Up @@ -80,7 +94,7 @@ public void updateQuest(UpdateVectorQuestDto dto) {
}

private Optional<Vector> findVector(Long id) {
return pineconeVectorStore.findById(id);
return vectorStore.findById(id);
}

private void saveVector(Vector vector, Long id) {
Expand Down Expand Up @@ -130,16 +144,30 @@ private List<String> updateList(List<String> existingList, String value, String
}

private Vector createVector(
Long memberId, String memberName, List<String> interestJobs, String goal, String quest) {
Long memberId,
String memberName,
List<String> interestJobs,
List<String> goals,
List<String> quests) {
return Vector.builder()
.memberId(memberId)
.memberName(memberName != null ? memberName : DEFAULT_MEMBER_NAME)
.interestJobs(interestJobs != null ? interestJobs : Collections.emptyList())
.goals(goal != null ? List.of(goal) : Collections.emptyList())
.quests(quest != null ? List.of(quest) : Collections.emptyList())
.goals(goals)
.quests(quests)
.build();
}

private Vector createVector(
Long memberId, String memberName, List<String> interestJobs, String goal, String quest) {
return createVector(
memberId,
memberName,
interestJobs,
goal != null ? List.of(goal) : Collections.emptyList(),
quest != null ? List.of(quest) : Collections.emptyList());
}

private Vector mergeVector(Vector existingVector, CreateVectorDto dto) {
return Vector.builder()
.memberId(existingVector.memberId())
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/com/groom/orbit/ai/dao/VectorStore.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.groom.orbit.ai.dao;

import java.util.List;
import java.util.Optional;

import com.google.protobuf.Struct;
import com.groom.orbit.ai.dao.vector.Vector;

public interface VectorStore {

void save(Long key, List<Float> vectors, Struct metadata);

Optional<Vector> findById(Long id);

List<Vector> findSimilar(List<Float> vector);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.groom.orbit.ai.dao;
package com.groom.orbit.ai.dao.pinecone;

import static com.groom.orbit.ai.app.util.PineconeConst.INDEX_NAME;
import static com.groom.orbit.ai.app.util.PineconeConst.INTEREST_JOB_NAMESPACE;
Expand All @@ -10,18 +10,22 @@

import com.google.protobuf.Struct;
import com.groom.orbit.ai.app.util.PineconeVectorMapper;
import com.groom.orbit.ai.dao.VectorStore;
import com.groom.orbit.ai.dao.vector.Vector;

import io.pinecone.clients.Index;
import io.pinecone.clients.Pinecone;
import io.pinecone.unsigned_indices_model.QueryResponseWithUnsignedIndices;

@Component
public class PineconeVectorStore {
public class PineconeVectorStore implements VectorStore {

private final Index index;
private final PineconeVectorMapper mapper;

private static final int SIMILAR_VECTOR_COUNT = 11;
private static final int FIND_VECTOR_COUNT = 1;

public PineconeVectorStore(Pinecone pinecone, PineconeVectorMapper mapper) {
this.index = pinecone.getIndexConnection(INDEX_NAME);
this.mapper = mapper;
Expand All @@ -33,16 +37,29 @@ public void save(Long key, List<Float> vectors, Struct metadata) {

public Optional<Vector> findById(Long id) {
String findKey = getId(id);
QueryResponseWithUnsignedIndices response = getQueryByVectorId(findKey);
QueryResponseWithUnsignedIndices response = getByVectorId(findKey);

return response.getMatchesList().stream()
.filter(match -> findKey.equals(match.getId()))
.findFirst()
.map(match -> mapper.fromStruct(match.getMetadata()));
}

private QueryResponseWithUnsignedIndices getQueryByVectorId(String findKey) {
return index.queryByVectorId(1, findKey, INTEREST_JOB_NAMESPACE, false, true);
public List<Vector> findSimilar(List<Float> vector) {
QueryResponseWithUnsignedIndices response = getQueryByVector(vector);

return response.getMatchesList().stream()
.map(match -> mapper.fromStruct(match.getMetadata()))
.toList();
}

private QueryResponseWithUnsignedIndices getQueryByVector(List<Float> vector) {
return index.queryByVector(
SIMILAR_VECTOR_COUNT, vector, INTEREST_JOB_NAMESPACE, null, false, true);
}

private QueryResponseWithUnsignedIndices getByVectorId(String findKey) {
return index.queryByVectorId(FIND_VECTOR_COUNT, findKey, INTEREST_JOB_NAMESPACE, false, true);
}

private void upsert(Long key, List<Float> vector, Struct metadata) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/groom/orbit/auth/app/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.groom.orbit.ai.VectorService;
import com.groom.orbit.ai.app.VectorService;
import com.groom.orbit.ai.app.dto.CreateVectorDto;
import com.groom.orbit.auth.app.dto.LoginResponseDto;
import com.groom.orbit.auth.dao.AuthMemberRepository;
Expand Down
Loading

0 comments on commit cf31401

Please sign in to comment.