Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f5116c1
feat: ignore empty contents
woowahan-neo Mar 3, 2025
40646e0
feat(answer-feedback): add visible column
woowahan-neo Mar 3, 2025
2d5b8cf
feat(answer-feedback): implement query answer feedback
woowahan-neo Mar 3, 2025
2903fd1
feat(answer-feedback): return feedback response with answer
woowahan-neo Mar 3, 2025
2a46b07
add temp controller
woowahan-neo Mar 3, 2025
c96f29b
add temp transactional
woowahan-neo Mar 3, 2025
5d2f5bf
feat(answer-feedback): retry query OpenAI on non-JSON response type
woowahan-neo Mar 3, 2025
b7e5b64
feat(feedback): remove final keyword
woowahan-neo Mar 3, 2025
20b4597
feat(feedback): remove temp code
woowahan-neo Mar 3, 2025
cc02d33
fix: 셀프 체크 작성 글자 수 제한 수정
gracefulBrown Mar 7, 2025
2d00c8f
fix: 프론트 배포 스크립트 수정
gracefulBrown Mar 7, 2025
aed6a16
fix: 프론트 배포 스크립트 수정
gracefulBrown Mar 7, 2025
cf97a06
feat: upgrade react-lottie version from 1.2.3 to ^1.2.10
woowahan-neo Mar 7, 2025
1523c99
feat: upgrade node version from 16.4.0 to 16.20.2
woowahan-neo Mar 7, 2025
9268b00
feat: exclude OpenAiAutoConfiguration
woowahan-neo Mar 7, 2025
ae3c755
fix: resolve babel-loader version conflict
woowahan-neo Mar 7, 2025
d74424c
feat: initial new data and fix bug
woowahan-neo Mar 10, 2025
fe498b2
feat(studylog): add feedback section to answers
woowahan-neo Mar 10, 2025
a3ee2c4
refactor(studylog): hide feedback body when not feedback
woowahan-neo Mar 10, 2025
d5fe7b5
Merge branch 'main' into feature/integrate-ai-feedback-qna
jaeyeonling Mar 10, 2025
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
1 change: 1 addition & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.ai:spring-ai-openai'
implementation 'org.springframework.ai:spring-ai-azure-openai-spring-boot-starter'
implementation 'org.springframework.retry:spring-retry'

testImplementation 'org.springframework.boot:spring-boot-starter-test'

Expand Down
322 changes: 295 additions & 27 deletions backend/src/main/java/wooteco/prolog/DataLoaderApplicationListener.java

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions backend/src/main/java/wooteco/prolog/RetryConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package wooteco.prolog;

import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;

@Configuration
@EnableRetry
public class RetryConfig {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
Expand All @@ -15,6 +14,8 @@
import wooteco.prolog.session.domain.QnaFeedbackRequest;
import wooteco.prolog.session.domain.repository.AnswerFeedbackRepository;

import java.util.List;

@Service
public class AnswerFeedbackService {

Expand All @@ -38,6 +39,11 @@ public void handleMemberUpdatedEvent(final AnswerUpdatedEvent event) {
log.debug("AnswerUpdatedEvent: {}", event);

final var answer = event.getAnswer();
if (answer.getContent().isEmpty() || answer.getQuestion().getMission().getGoal().isEmpty()) {
log.debug("Answer content or mission goal is empty: {}", answer);
return;
}

final var feedbackRequest = new QnaFeedbackRequest(
answer.getQuestion().getMission().getGoal(),
answer.getQuestion().getContent(),
Expand All @@ -52,5 +58,11 @@ public void handleMemberUpdatedEvent(final AnswerUpdatedEvent event) {
);

answerFeedbackRepository.save(answerFeedback);
log.debug("AnswerFeedback saved: {}", answerFeedback);
}

@Transactional(readOnly = true)
public List<AnswerFeedback> findRecentByMemberIdAndQuestionIds(final Long memberId, final List<Long> questionIds) {
return answerFeedbackRepository.findRecentByMemberIdAndQuestionIdsAndVisible(memberId, questionIds);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package wooteco.prolog.session.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
Expand Down Expand Up @@ -31,10 +32,41 @@ public class AnswerFeedback {
@Embedded
private QnaFeedbackContents contents;

public AnswerFeedback(Question question, Long memberId, QnaFeedbackRequest request, QnaFeedbackContents contents) {
@Column
private boolean visible;

public AnswerFeedback(
final Question question,
final Long memberId,
final QnaFeedbackRequest request,
final QnaFeedbackContents contents
) {
this(question, memberId, request, contents, false);
}

public AnswerFeedback(
final Question question,
final Long memberId,
final QnaFeedbackRequest request,
final QnaFeedbackContents contents,
final boolean visible
) {
this.question = question;
this.memberId = memberId;
this.request = request;
this.contents = contents;
this.visible = visible;
}

public String getStrengths() {
return contents.strengths();
}

public String getImprovementPoints() {
return contents.improvementPoints();
}

public String getAdditionalLearning() {
return contents.additionalLearning();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ public class Question {
@ManyToOne
private Mission mission;

public Question(final String content, final Mission mission) {
this.content = content;
this.mission = mission;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
package wooteco.prolog.session.domain.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import wooteco.prolog.session.domain.AnswerFeedback;

import java.util.List;

public interface AnswerFeedbackRepository extends JpaRepository<AnswerFeedback, Long> {

@Query("""
SELECT af
FROM AnswerFeedback af
JOIN (
SELECT MAX(id) AS id
FROM AnswerFeedback
WHERE memberId = :memberId AND question.id IN (:questionIds) AND visible = TRUE
GROUP BY question.id
) sub
ON af.id = sub.id
""")
List<AnswerFeedback> findRecentByMemberIdAndQuestionIdsAndVisible(Long memberId, List<Long> questionIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.Resource;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;
import wooteco.prolog.session.domain.QnaFeedbackContents;
import wooteco.prolog.session.domain.QnaFeedbackProvider;
Expand All @@ -22,7 +25,7 @@

@Profile({"prod", "dev"})
@Component
public final class AzureOpenAiFeedbackProvider implements QnaFeedbackProvider {
public class AzureOpenAiFeedbackProvider implements QnaFeedbackProvider {

private static final Logger log = LoggerFactory.getLogger(AzureOpenAiFeedbackProvider.class);

Expand Down Expand Up @@ -61,6 +64,12 @@ public final class AzureOpenAiFeedbackProvider implements QnaFeedbackProvider {
this.template = new PromptTemplate(resource);
}

@Retryable(
retryFor = RuntimeJsonProcessingException.class,
backoff = @Backoff(delay = 1000),
maxAttempts = 4,
recover = "logging"
)
@Override
public QnaFeedbackContents evaluate(final QnaFeedbackRequest request) {
log.debug("Requesting feedback evaluation [request={}]", request);
Expand Down Expand Up @@ -88,7 +97,18 @@ public QnaFeedbackContents evaluate(final QnaFeedbackRequest request) {
return objectMapper.readValue(responseText, QnaFeedbackContents.class);
} catch (final JsonProcessingException e) {
log.error("Failed to parse response from chat model [responseText={}]", responseText, e);
throw new RuntimeException("Invalid response format from AI model", e);
throw new RuntimeJsonProcessingException("Invalid response format from AI model", e);
}
}

@Recover
void logging(final RuntimeException e, final QnaFeedbackRequest request) {
log.error("Failed to evaluate feedback [request={}]", request, e);
}

private static class RuntimeJsonProcessingException extends RuntimeException {
public RuntimeJsonProcessingException(final String message, final Throwable cause) {
super(message, cause);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,5 @@
package wooteco.prolog.studylog.application;

import static java.time.temporal.TemporalAdjusters.firstDayOfMonth;
import static java.time.temporal.TemporalAdjusters.lastDayOfMonth;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static wooteco.prolog.common.exception.BadRequestCode.MEMBER_NOT_ALLOWED;
import static wooteco.prolog.common.exception.BadRequestCode.STUDYLOG_ARGUMENT;
import static wooteco.prolog.common.exception.BadRequestCode.STUDYLOG_DOCUMENT_NOT_FOUND;
import static wooteco.prolog.common.exception.BadRequestCode.STUDYLOG_NOT_FOUND;
import static wooteco.prolog.common.exception.BadRequestCode.STUDYLOG_SCRAP_NOT_EXIST_EXCEPTION;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -33,10 +16,12 @@
import wooteco.prolog.member.domain.Member;
import wooteco.prolog.member.domain.Role;
import wooteco.prolog.organization.application.OrganizationService;
import wooteco.prolog.session.application.AnswerFeedbackService;
import wooteco.prolog.session.application.AnswerService;
import wooteco.prolog.session.application.MissionService;
import wooteco.prolog.session.application.SessionService;
import wooteco.prolog.session.domain.Answer;
import wooteco.prolog.session.domain.AnswerFeedback;
import wooteco.prolog.session.domain.AnswerTemp;
import wooteco.prolog.session.domain.Mission;
import wooteco.prolog.session.domain.Session;
Expand Down Expand Up @@ -66,6 +51,24 @@
import wooteco.prolog.studylog.domain.repository.dto.CommentCount;
import wooteco.prolog.studylog.event.StudylogDeleteEvent;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static java.time.temporal.TemporalAdjusters.firstDayOfMonth;
import static java.time.temporal.TemporalAdjusters.lastDayOfMonth;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static wooteco.prolog.common.exception.BadRequestCode.MEMBER_NOT_ALLOWED;
import static wooteco.prolog.common.exception.BadRequestCode.STUDYLOG_ARGUMENT;
import static wooteco.prolog.common.exception.BadRequestCode.STUDYLOG_DOCUMENT_NOT_FOUND;
import static wooteco.prolog.common.exception.BadRequestCode.STUDYLOG_NOT_FOUND;
import static wooteco.prolog.common.exception.BadRequestCode.STUDYLOG_SCRAP_NOT_EXIST_EXCEPTION;

@Service
@AllArgsConstructor
@Transactional(readOnly = true)
Expand All @@ -78,6 +81,7 @@ public class StudylogService {
private final MemberService memberService;
private final TagService tagService;
private final AnswerService answerService;
private final AnswerFeedbackService answerFeedbackService;
private final SessionService sessionService;
private final MissionService missionService;
private final OrganizationService organizationService;
Expand Down Expand Up @@ -311,10 +315,15 @@ public StudylogResponse retrieveStudylogById(LoginMember loginMember, Long study
Studylog studylog = findStudylogById(studylogId);

List<Answer> answers = answerService.findAnswersByStudylogId(studylog.getId());
List<Long> questionIds = answers.stream()
.mapToLong(it -> it.getQuestion().getId())
.boxed()
.toList();
List<AnswerFeedback> answerFeedbacks = answerFeedbackService.findRecentByMemberIdAndQuestionIds(loginMember.getId(), questionIds);

onStudylogRetrieveEvent(loginMember, studylog, isViewed);

return toStudylogResponse(loginMember, studylog, answers);
return toStudylogResponse(loginMember, studylog, answers, answerFeedbacks);
}

@Transactional
Expand Down Expand Up @@ -362,14 +371,14 @@ private void onStudylogRetrieveEvent(LoginMember loginMember, Studylog studylog,
}
}

private StudylogResponse toStudylogResponse(LoginMember loginMember, Studylog studylog, List<Answer> answers) {
private StudylogResponse toStudylogResponse(LoginMember loginMember, Studylog studylog, List<Answer> answers, List<AnswerFeedback> answerFeedbacks) {
boolean liked = studylog.likedByMember(loginMember.getId());
boolean read = studylogReadRepository.findByMemberIdAndStudylogId(loginMember.getId(), studylog.getId())
.isPresent();
boolean scraped = studylogScrapRepository.findByMemberIdAndStudylogId(loginMember.getId(), studylog.getId())
.isPresent();

return StudylogResponse.of(studylog, answers, scraped, read, liked);
return StudylogResponse.of(studylog, answers, answerFeedbacks, scraped, read, liked);
}

public StudylogResponse findByIdAndReturnStudylogResponse(Long id) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package wooteco.prolog.studylog.application.dto;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.lang.Nullable;
import wooteco.prolog.session.domain.Answer;
import wooteco.prolog.session.domain.AnswerFeedback;
import wooteco.prolog.session.domain.AnswerTemp;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@AllArgsConstructor
@NoArgsConstructor
@Getter
Expand All @@ -18,24 +22,63 @@ public class AnswerResponse {
private String answerContent;
private Long questionId;
private String questionContent;
@Nullable
private String strengths;
@Nullable
private String improvementPoints;
@Nullable
private String additionalLearning;

public AnswerResponse(
final Long id,
final String answerContent,
final Long questionId,
final String questionContent
) {
this.id = id;
this.answerContent = answerContent;
this.questionId = questionId;
this.questionContent = questionContent;
}

public static List<AnswerResponse> emptyListOf() {
return new ArrayList<>();
}

public static List<AnswerResponse> listOf(List<Answer> answers) {
return listOf(answers, List.of());
}

public static List<AnswerResponse> listOf(List<Answer> answers, List<AnswerFeedback> answerFeedbacks) {
return listOf(answers, answerFeedbacks.stream()
.collect(Collectors.toMap(answerFeedback -> answerFeedback.getQuestion().getId(), answerFeedback -> answerFeedback)));
}

public static List<AnswerResponse> listOf(List<Answer> answers, Map<Long, AnswerFeedback> answerFeedbacks) {
if (answers == null || answers.isEmpty()) {
return emptyListOf();
}

return answers.stream()
.map(answer -> new AnswerResponse(answer.getId(), answer.getContent(), answer.getQuestion().getId(),
answer.getQuestion().getContent()))
.map(answer -> of(answer, answerFeedbacks.get(answer.getQuestion().getId())))
.collect(Collectors.toList());
}

public static AnswerResponse of(AnswerTemp answerTemp) {
return new AnswerResponse(answerTemp.getId(), answerTemp.getContent(), answerTemp.getQuestion().getId(),
answerTemp.getQuestion().getContent());
}

public static AnswerResponse of(Answer answer) {
return new AnswerResponse(answer.getId(), answer.getContent(), answer.getQuestion().getId(),
answer.getQuestion().getContent());
}

public static AnswerResponse of(Answer answer, AnswerFeedback answerFeedback) {
if (answerFeedback == null) {
return of(answer);
}
return new AnswerResponse(answer.getId(), answer.getContent(), answer.getQuestion().getId(),
answer.getQuestion().getContent(), answerFeedback.getStrengths(), answerFeedback.getImprovementPoints(), answerFeedback.getAdditionalLearning());
}
}
Loading
Loading