Skip to content
This repository has been archived by the owner on Oct 20, 2024. It is now read-only.

Commit

Permalink
feat: ✨ 챗봇 임베딩 및 대화 기능 구현 완료 (#87)
Browse files Browse the repository at this point in the history
* Initial commit

* Feat/#1 oauth2login (#3)

* feat: User 엔터티 생성

* feat: jwt 버전 11->12, JWTUtil 생성

* feat: JWTFilter(JwtAuthenticationFilter) 등록

* feat: kakao 로그인 구현

* docs: swagger 태그(Authorization) 추가 (#5)

* feat: User 엔터티 생성

* feat: jwt 버전 11->12, JWTUtil 생성

* feat: JWTFilter(JwtAuthenticationFilter) 등록

* feat: kakao 로그인 구현

* docs: swagger 태그(Authorization) 추가

* feat: accesstoken 테스트를 위한 test login 생성 (#9)

* feat: User 엔티티에 상속 (#12)

* feat: BaseEntity 생성

* feat: User 엔티티에 상속

* feat: 일기 생성 기능 구현 (#14)

* feat: accesstoken 테스트를 위한 test login 생성

* feat: 일기 생성 기능 구현

* hotfix: ci 에러 수정 (#16)

* feat: accesstoken 테스트를 위한 test login 생성

* feat: 일기 생성 기능 구현

* hotfix: ci 에러 수정

* fix: OIDC 카카오 로그인 nullPointerException 해결

* feat: 닉네임 설정 기능 구현 (#21)

* feat: 일기 수정 기능 구현 (#25)

* feat: 일기에 감정 컬럼 추가

* feat: 일기 수정 기능 구현

* feat: 일기 삭제 기능 구현 (#27)

* feat: 일기에 감정 컬럼 추가

* feat: 일기 수정 기능 구현

* feat: 일기 삭제 기능 구현

* feat: 일기 감정 분석 기능 구현 (#31)

* feat: 감정 저장 기능 구현 (#33)

* feat: 일기 감정 분석 기능 구현

* feat: 감정 저장 기능 구현

* fix: 🐛 감정 저장 안되던 오류 수정 (#35)

* feat: 일기 감정 분석 기능 구현

* feat: 감정 저장 기능 구현

* fix: 🐛 감정 저장 안되던 오류 수정

* hotfix: 🚑 서버 꺼짐 현상 해결 (#37)

* feat: 일기 감정 분석 기능 구현

* feat: 감정 저장 기능 구현

* fix: 🐛 감정 저장 안되던 오류 수정

* hotfix: 🚑 서버 꺼짐 현상 해결

* feat: ✨ 홈 화면 조회 기능 구현 (#41)

* feat: ✨ 회원가입 완료 여부 필드 추가 (#44)

* feat: ✨ 일기 상세 조회 구현 (#47)

* feat: ✨ 기간 별 감정 통계 조회 기능 구현 (#50)

* feat: ✨ 일기 내용 검색 기능 구현 (#52)

* feat: ✨ 감정 별 일기 조회 (#54)

* feat: ✨ 월 별 일기 조회 기능 구현 (#59)

* ci: ⚡ workflow 수정 (#61)

* ci: ⚡ workflow 수정

* ci: ⚡ workflow 수정

* feat: ✨ user 엔터티 fcmToken 컬럼 추가, 로그인 시 토큰 최신화 구현 (#63)

* ci: ⚡ workflow 수정

* ci: ⚡ workflow 수정

* feat: ✨ fcm 토큰 알림 기능 구현

* feat: ✨ user 엔터티 fcmToken 컬럼 추가, 로그인 시 토큰 최신화 구현

* feat: ✨ 북마크 추가/삭제 기능 구현, 일기/홈화면 조회 쿼리문 수정 (#65)

* feat: ✨ 북마크 추가 기능 구현

* feat: ✨ 북마크 추가/삭제 기능 구현, 일기/홈화면 조회 쿼리문 수정

* feat: 🚀 fcmtoken 등록 api 분리 (#68)

* feat: ✨ 유저 정보 조회 기능 구현 (#71)

* Feat/#70 user info (#73)

* feat: ✨ 유저 정보 조회 기능 구현

* hotfix: 🚑 cd 에러 해결

* refactor: 🚀 gpt prompt 수정 (#76)

* feat: ✨ 일기 요약 스케줄러 구현 (#80)

* refactor: 🚀 엔터티 접근 지정자 수정 (#84)

* feat: ✨ 챗봇 임베딩 및 대화 기능 구현 완료 (#86)
  • Loading branch information
LEEJaeHyeok97 authored Aug 3, 2024
1 parent 9b5d7b5 commit e8295f5
Show file tree
Hide file tree
Showing 13 changed files with 343 additions and 0 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ dependencies {
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"


implementation 'org.json:json:20210307'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.aidiary.domain.chatbot.application;

import com.aidiary.domain.chatbot.domain.ChatHistory;
import com.aidiary.domain.chatbot.domain.ChatRole;
import com.aidiary.domain.chatbot.domain.repository.ChatHistoryRepository;
import com.aidiary.domain.chatbot.dto.DiaryEmbeddingReq;
import com.aidiary.domain.chatbot.dto.QueryReq;
import com.aidiary.domain.summary.domain.DiarySummary;
import com.aidiary.domain.summary.domain.repository.DiarySummaryRepository;
import com.aidiary.domain.user.domain.User;
import com.aidiary.domain.user.domain.repository.UserRepository;
import com.aidiary.global.config.security.token.UserPrincipal;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class ChatbotService {

private final ChatHistoryRepository chatHistoryRepository;
private final DiarySummaryRepository diarySummaryRepository;
private final UserRepository userRepository;


@Transactional
public DiaryEmbeddingReq makeRequest(Long id) {
User user = userRepository.findById(id)
.orElseThrow(EntityNotFoundException::new);
DiarySummary diarySummary = diarySummaryRepository.findByUser(user)
.orElseThrow(EntityNotFoundException::new);

DiaryEmbeddingReq diaryEmbeddingReq = DiaryEmbeddingReq.builder()
.userId(id.toString())
.summarizedDiary(diarySummary.getSummarizedDiary())
.build();

return diaryEmbeddingReq;
}

@Transactional
public QueryReq makeQuery(UserPrincipal userPrincipal, String question) {
User user = userRepository.findById(userPrincipal.getId())
.orElseThrow(EntityNotFoundException::new);


// 챗 히스토리를 최근 20개 이하로 가져온다
List<ChatHistory> recentChatHistoryByUserId = chatHistoryRepository.findRecentChatHistoryByUserId(userPrincipal.getId());
List<String> chatHistoryList = recentChatHistoryByUserId.stream()
.map(chat -> String.format("%s: %s", chat.getChatRole() == ChatRole.USER ? "사용자" : "친구", chat.getMessage()))
.toList();

log.info("User ID: {}", userPrincipal.getId());
log.info("Question: {}", question);
log.info("Chat History: {}", chatHistoryList);

ChatHistory chatHistory = ChatHistory.builder()
.user(user)
.message(question)
.chatRole(ChatRole.USER)
.build();


chatHistoryRepository.save(chatHistory);

QueryReq queryReq = QueryReq.builder()
.userId(userPrincipal.getId().toString())
.question(question)
.chatHistory(chatHistoryList)
.build();

return queryReq;

}

@Transactional
public void registerBotChat(UserPrincipal userPrincipal, String result) {
User user = userRepository.findById(userPrincipal.getId())
.orElseThrow(EntityNotFoundException::new);

ChatHistory chatHistory = ChatHistory.builder()
.user(user)
.message(result)
.chatRole(ChatRole.BOT)
.build();
chatHistoryRepository.save(chatHistory);
}
}
37 changes: 37 additions & 0 deletions src/main/java/com/aidiary/domain/chatbot/domain/ChatHistory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.aidiary.domain.chatbot.domain;

import com.aidiary.domain.common.BaseEntity;
import com.aidiary.domain.user.domain.User;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ChatHistory extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "user_id")
private User user;

private String message;

@Enumerated(EnumType.STRING)
@Column(columnDefinition = "VARCHAR(10)")
private ChatRole chatRole;


@Builder
public ChatHistory(User user, String message, ChatRole chatRole) {
this.user = user;
this.message = message;
this.chatRole = chatRole;
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/aidiary/domain/chatbot/domain/ChatRole.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.aidiary.domain.chatbot.domain;

public enum ChatRole {
USER,
BOT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.aidiary.domain.chatbot.domain.repository;

import com.aidiary.domain.chatbot.domain.ChatHistory;

import java.util.List;

public interface ChatHistoryQueryDslRepository {
List<ChatHistory> findRecentChatHistoryByUserId(Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.aidiary.domain.chatbot.domain.repository;

import com.aidiary.domain.chatbot.domain.ChatHistory;
import com.aidiary.domain.chatbot.domain.QChatHistory;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;

import static com.aidiary.domain.chatbot.domain.QChatHistory.chatHistory;

@RequiredArgsConstructor
@Repository
public class ChatHistoryQueryDslRepositoryImpl implements ChatHistoryQueryDslRepository {

private final JPAQueryFactory queryFactory;

@Override
public List<ChatHistory> findRecentChatHistoryByUserId(Long id) {
return queryFactory
.select(chatHistory)
.from(chatHistory)
.where(chatHistory.user.id.eq(id))
.orderBy(chatHistory.createdAt.desc())
.limit(20)
.fetch();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.aidiary.domain.chatbot.domain.repository;

import com.aidiary.domain.chatbot.domain.ChatHistory;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ChatHistoryRepository extends JpaRepository<ChatHistory, Long>, ChatHistoryQueryDslRepository {
}
9 changes: 9 additions & 0 deletions src/main/java/com/aidiary/domain/chatbot/dto/ChatReq.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.aidiary.domain.chatbot.dto;

import lombok.Builder;

@Builder
public record ChatReq(
String question
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.aidiary.domain.chatbot.dto;

import lombok.Builder;

import java.util.List;

@Builder
public record DiaryEmbeddingReq(
String userId,
String summarizedDiary
) {
}
13 changes: 13 additions & 0 deletions src/main/java/com/aidiary/domain/chatbot/dto/QueryReq.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.aidiary.domain.chatbot.dto;

import lombok.Builder;

import java.util.List;

@Builder
public record QueryReq(
String userId,
String question,
List<String> chatHistory
) {
}
11 changes: 11 additions & 0 deletions src/main/java/com/aidiary/domain/chatbot/dto/QueryRes.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.aidiary.domain.chatbot.dto;

import lombok.Builder;

import java.util.List;

@Builder
public record QueryRes(
String result
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.aidiary.domain.chatbot.presentation;

import com.aidiary.domain.chatbot.application.ChatbotService;
import com.aidiary.domain.chatbot.dto.ChatReq;
import com.aidiary.domain.chatbot.dto.DiaryEmbeddingReq;
import com.aidiary.domain.chatbot.dto.QueryReq;
import com.aidiary.global.config.security.token.CurrentUser;
import com.aidiary.global.config.security.token.UserPrincipal;
import com.aidiary.global.payload.ErrorResponse;
import com.aidiary.global.payload.Message;
import com.aidiary.global.payload.ResponseCustom;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@Tag(name = "Chatbot", description = "Chatbot API")
@RequiredArgsConstructor
@RestController
@RequestMapping("/chatbot")
@Slf4j
public class ChatbotController {

private final ChatbotService chatbotService;
private final RestTemplate restTemplate = new RestTemplate();

@Value("${chatbot.fastApi.addDiaryUrl}")
private String addDiaryUrl;

@Value("${chatbot.fastApi.queryUrl}")
private String queryUrl;


@Operation(summary = "일기 임베딩", description = "유저의 일기를 AI 서버에 임베딩합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "일기 임베딩 성공", content = {@Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = String.class)))}),
@ApiResponse(responseCode = "400", description = "일기 임베딩 실패", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}),
})
@PostMapping("/embedding")
public ResponseCustom<?> addEmbedding(
@Parameter(description = "Accesstoken을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal
) {
String fastApiUrl = addDiaryUrl;

HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");

DiaryEmbeddingReq madeRequest = chatbotService.makeRequest(userPrincipal.getId());

HttpEntity<DiaryEmbeddingReq> requestEntity = new HttpEntity<>(madeRequest, headers);
ResponseEntity<String> response = restTemplate.postForEntity(fastApiUrl, requestEntity, String.class);

JSONObject responseBody = new JSONObject(response.getBody());
String message = responseBody.getString("message");

return ResponseCustom.OK(message);
}

@Operation(summary = "챗봇과 대화하기", description = "임베딩된 일기를 바탕으로 챗봇과 대화합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "챗봇 대화 성공", content = {@Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = String.class)))}),
@ApiResponse(responseCode = "400", description = "챗봇 대화 실패", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}),
})
@PostMapping("/chat")
public ResponseCustom<?> chat(
@Parameter(description = "Accesstoken을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal,
@RequestBody ChatReq chatReq
) {
String fastApiUrl = queryUrl;

HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");

QueryReq queryReq = chatbotService.makeQuery(userPrincipal, chatReq.question());

log.info("Requesting FastAPI with QueryReq: {}", queryReq);

HttpEntity<QueryReq> requestEntity = new HttpEntity<>(queryReq, headers);
ResponseEntity<String> response = restTemplate.postForEntity(fastApiUrl, requestEntity, String.class);

// JSON 응답에서 message 필드만 추출
JSONObject responseBody = new JSONObject(response.getBody());
String message = responseBody.getString("message");

// Bot 응답 저장
chatbotService.registerBotChat(userPrincipal, message);

return ResponseCustom.OK(message);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.aidiary.domain.summary.domain.repository;

import com.aidiary.domain.summary.domain.DiarySummary;
import com.aidiary.domain.user.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface DiarySummaryRepository extends JpaRepository<DiarySummary, Long> {
Optional<DiarySummary> findByUser(User user);
}

0 comments on commit e8295f5

Please sign in to comment.