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

Test #47

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open

Test #47

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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ out/
/src/main/resources/database/**
/src/main/resources/oauth2/**
/src/main/resources/s3/**
/src/main/resources/chatgpt/**

3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ dependencies {
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

// GPT
implementation 'io.github.flashvayne:chatgpt-spring-boot-starter:1.0.4'

// Amazon S3
implementation platform('com.amazonaws:aws-java-sdk-bom:1.11.1000')
implementation 'com.amazonaws:aws-java-sdk-s3'
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/example/ai_tutor/AiTutorApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
@PropertySource(value = { "classpath:oauth2/application-oauth2.yml" }, factory = YamlPropertySourceFactory.class)
@PropertySource(value = { "classpath:database/application-database.yml" }, factory = YamlPropertySourceFactory.class)
@PropertySource(value = { "classpath:s3/application-s3.yml" }, factory = YamlPropertySourceFactory.class)
@PropertySource(value = { "classpath:chatgpt/application-chatgpt.yml" }, factory = YamlPropertySourceFactory.class)
public class AiTutorApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.example.ai_tutor.domain.chatgpt.application;

import com.example.ai_tutor.global.config.ChatGptConfig;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.*;

@Service
@RequiredArgsConstructor
public class GptService {

private final Map<Long, List<Map<String, String>>> noteChatHistories = new HashMap<>();
private final Map<Long, String> noteOriginalTexts = new HashMap<>();
private final Map<Long, Integer> messageCounts = new HashMap<>(); // ๋ฉ”์‹œ์ง€ ์นด์šดํŠธ

@Value("${chatgpt.api-key}")
private String apiKey;

// ๋Œ€ํ™” ์‹œ์ž‘ ์‹œ ์›๋ฌธ์„ ํฌํ•จ์‹œํ‚ค๋Š” ๋ฉ”์†Œ๋“œ
public void startConversation(Long noteId, String originalText) {
noteOriginalTexts.put(noteId, originalText);
clearChatHistory(noteId); // ๋Œ€ํ™” ๋‚ด์—ญ ์ดˆ๊ธฐํ™”
// ์›๋ฌธ ์„ค๋ช… ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
addOriginalTextToChatHistory(noteId);
}

private void addOriginalTextToChatHistory(Long noteId) {
String originalText = noteOriginalTexts.get(noteId);
if (originalText != null) {
List<Map<String, String>> chatHistory = noteChatHistories.computeIfAbsent(noteId, k -> new ArrayList<>());
Map<String, String> originalTextIntro = new HashMap<>();
originalTextIntro.put("role", "system");
originalTextIntro.put("content", "๋‹ค์Œ์€ ์›๋ฌธ์˜ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค:\n" + originalText);
chatHistory.add(originalTextIntro);
}
}

public JsonNode callChatGpt(Long noteId, String userMsg) throws JsonProcessingException {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(apiKey);

ObjectMapper objectMapper = new ObjectMapper();

Map<String, Object> bodyMap = new HashMap<>();
bodyMap.put("model", ChatGptConfig.MODEL);
bodyMap.put("max_tokens", ChatGptConfig.MAX_TOKEN);

// ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€์™€ ์ปจํ…์ŠคํŠธ ์ถ”๊ฐ€
List<Map<String, String>> chatHistory = noteChatHistories.computeIfAbsent(noteId, k -> new ArrayList<>());
Map<String, String> userMessage = new HashMap<>();
userMessage.put("role", "user");
userMessage.put("content", ChatGptConfig.CONTENT + userMsg);
chatHistory.add(userMessage);

bodyMap.put("messages", chatHistory);

String body = objectMapper.writeValueAsString(bodyMap);

HttpEntity<String> request = new HttpEntity<>(body, headers);

RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(ChatGptConfig.URL, HttpMethod.POST, request, String.class);

JsonNode responseBody = objectMapper.readTree(response.getBody());

// ๋Œ€ํ™” ๋‚ด์—ญ ์—…๋ฐ์ดํŠธ
Map<String, String> assistantResponse = new HashMap<>();
assistantResponse.put("role", "assistant");
assistantResponse.put("content", responseBody.path("choices").get(0).path("message").path("content").asText());
chatHistory.add(assistantResponse);

// ๋ฉ”์„ธ์ง€ ์นด์šดํŠธ (15ํšŒ๋งˆ๋‹ค ์›๋ฌธ ์ฃผ์ž…)
int count = messageCounts.getOrDefault(noteId, 0) + 1;
messageCounts.put(noteId, count);

if (count % 15 == 0) {
addOriginalTextToChatHistory(noteId);
}

return responseBody;
}

public String getAssistantMsg(Long noteId, String userMsg) throws JsonProcessingException {
JsonNode jsonNode = callChatGpt(noteId, userMsg);
return jsonNode.path("choices").get(0).path("message").path("content").asText();
}

// ๋Œ€ํ™” ๋‚ด์—ญ ์ดˆ๊ธฐํ™”
public void clearChatHistory(Long noteId) {
noteChatHistories.remove(noteId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ public ResponseEntity<?> getQuestionsAndAnswers(UserPrincipal userPrincipal, Lon
.sorted(Comparator.comparing(PracticeResultsRes::getSequence))
.collect(Collectors.toList());

// test

ApiResponse apiResponse = ApiResponse.builder()
.check(true)
.information(practiceResultsRes)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.example.ai_tutor.domain.practice.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class TutorRecordRes {

@Schema( type = "String", example ="(์ถ”ํ›„ ์ถ”๊ฐ€)", description="ํŠน์ • ๋‹ต๋ณ€์— ๋Œ€ํ•œ ํŠœํ„ฐ์˜ tts ๋…น์ŒํŒŒ์ผ ์ฃผ์†Œ์ž…๋‹ˆ๋‹ค.")
private String tutorRecordUrl;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.example.ai_tutor.domain.tutor.application;

import com.example.ai_tutor.domain.chatgpt.application.GptService;
import com.example.ai_tutor.domain.note.domain.Note;
import com.example.ai_tutor.domain.note.domain.repository.NoteRepository;
import com.example.ai_tutor.domain.tutor.domain.Tutor;
import com.example.ai_tutor.domain.tutor.domain.repository.TutorRepository;
import com.example.ai_tutor.domain.tutor.dto.request.QuestionReq;
import com.example.ai_tutor.domain.tutor.dto.response.AnswerRes;
import com.example.ai_tutor.domain.tutor.dto.response.TutorRes;
import com.example.ai_tutor.domain.user.domain.User;
import com.example.ai_tutor.domain.user.domain.repository.UserRepository;
import com.example.ai_tutor.global.DefaultAssert;
import com.example.ai_tutor.global.config.security.token.UserPrincipal;
import com.example.ai_tutor.global.payload.ApiResponse;
import com.example.ai_tutor.global.payload.Message;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
@RequiredArgsConstructor
public class TutorService {

private final UserRepository userRepository;
private final NoteRepository noteRepository;
private final TutorRepository tutorRepository;
private final GptService gptService;

// ์ตœ์ดˆ ํ˜ธ์ถœ ์‹œ ์›๋ฌธ ๋‚ด์šฉ ์ฃผ์ž…
public ResponseEntity<?> initializeChatting(UserPrincipal userPrincipal, Long noteId) {
Optional<Note> noteOptional = noteRepository.findById(noteId);
DefaultAssert.isTrue(noteOptional.isPresent(), "ํ•ด๋‹น ๋…ธํŠธ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
Note note = noteOptional.get();

gptService.startConversation(noteId, note.getOriginalText());

ApiResponse apiResponse = ApiResponse.builder()
.check(true)
.information(Message.builder().message("์›๋ฌธ์ด ์ฃผ์ž…๋˜์—ˆ์Šต๋‹ˆ๋‹ค.").build())
.build();
return ResponseEntity.ok(apiResponse);
}

@Transactional
public ResponseEntity<?> questionToTutor(UserPrincipal userPrincipal, Long noteId, QuestionReq questionReq) throws JsonProcessingException {
Optional<User> userOptional = userRepository.findById(userPrincipal.getId());
DefaultAssert.isTrue(userOptional.isPresent(), "ํ•ด๋‹น ์‚ฌ์šฉ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
User user = userOptional.get();

Optional<Note> noteOptional = noteRepository.findById(noteId);
DefaultAssert.isTrue(noteOptional.isPresent(), "ํ•ด๋‹น ๋…ธํŠธ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
Note note = noteOptional.get();

DefaultAssert.isTrue(Objects.equals(note.getUser().getUserId(), userPrincipal.getId()), "์ž˜๋ชป๋œ ์ ‘๊ทผ์ž…๋‹ˆ๋‹ค.");
// ์ฑ—์ง€ํ”ผํ‹ฐ ํ˜ธ์ถœ
String answer = gptService.getAssistantMsg(noteId, questionReq.getQuestion());
// ์งˆ๋ฌธ ์ €์žฅ
// ๋‹ต๋ณ€ ์ €์žฅ
Tutor tutor = Tutor.builder()
.question(questionReq.getQuestion())
.answer(answer) // ํŠœํ„ฐ์˜ ๋‹ต๋ณ€ ์ €์žฅ
.folder(note.getFolder())
.note(note)
.user(user).build();
tutorRepository.save(tutor);
// response ์ƒ์„ฑ
AnswerRes answerRes = AnswerRes.builder()
.answer(tutor.getAnswer())
.build();

ApiResponse apiResponse = ApiResponse.builder()
.check(true)
.information(answerRes)
.build();
return ResponseEntity.ok(apiResponse);
}

// ๊ธฐ์กด ํ•™์Šต ์งˆ๋ฌธ ๋ฐ ๋‹ต๋ณ€ ์กฐํšŒ
public ResponseEntity<?> getQuestionAndAnswer(UserPrincipal userPrincipal, Long noteId) {
Optional<Note> noteOptional = noteRepository.findById(noteId);
DefaultAssert.isTrue(noteOptional.isPresent(), "ํ•ด๋‹น ๋…ธํŠธ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
Note note = noteOptional.get();
// ์‚ฌ์šฉ์ž ๊ฒ€์ฆ
DefaultAssert.isTrue(Objects.equals(note.getUser().getUserId(), userPrincipal.getId()), "์ž˜๋ชป๋œ ์ ‘๊ทผ์ž…๋‹ˆ๋‹ค.");

List<Tutor> tutors = tutorRepository.findByNoteOrderByCreatedAt(note);

List<TutorRes> tutorResList = tutors.stream()
.map(tutor -> TutorRes.builder()
.tutorId(tutor.getTutorId())
.question(tutor.getQuestion())
.answer(tutor.getAnswer())
.build())
.toList();

ApiResponse apiResponse = ApiResponse.builder()
.check(true)
.information(tutorResList)
.build();

return ResponseEntity.ok(apiResponse);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class Tutor extends BaseEntity {
@JoinColumn(name="folder_id")
private Folder folder;

@OneToOne(fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="note_id")
private Note note;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.ai_tutor.domain.tutor.domain.repository;

import com.example.ai_tutor.domain.note.domain.Note;
import com.example.ai_tutor.domain.tutor.domain.Tutor;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface TutorRepository extends JpaRepository<Tutor, Long> {
List<Tutor> findByNoteOrderByCreatedAt(Note note);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.ai_tutor.domain.tutor.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;

@Getter
public class QuestionReq {

@Schema( type = "String", example ="๊ธฐ๋ง๊ณ ์‚ฌ ์ž˜ ๋ณด๋Š” ๋ฐฉ๋ฒ• ์•Œ๋ ค์ค˜", description="์งˆ๋ฌธ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค.")
public String question;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.ai_tutor.domain.tutor.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class AnswerRes {

@Schema( type = "String", example ="๊ณ„ํš์ ์œผ๋กœ ๊ณต๋ถ€ํ•˜๊ณ  ์ถฉ๋ถ„ํ•œ ํœด์‹์„ ์ทจํ•˜์„ธ์š”.", description="์งˆ๋ฌธ์— ๋Œ€ํ•œ ๋‹ต๋ณ€์ž…๋‹ˆ๋‹ค.")
public String answer;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.ai_tutor.domain.tutor.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class TutorRes {

@Schema( type = "Long", example ="1", description="์ฑ—๋ด‡ ์งˆ๋ฌธ/๋‹ต๋ณ€์˜ id์ž…๋‹ˆ๋‹ค.")
public Long tutorId;

@Schema( type = "String", example ="๊ธฐ๋ง๊ณ ์‚ฌ ์ž˜ ๋ณด๋Š” ๋ฐฉ๋ฒ• ์•Œ๋ ค์ค˜", description="์งˆ๋ฌธ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค.")
public String question;

@Schema( type = "String", example ="๊ณ„ํš์ ์œผ๋กœ ๊ณต๋ถ€ํ•˜๊ณ  ์ถฉ๋ถ„ํ•œ ํœด์‹์„ ์ทจํ•˜์„ธ์š”.", description="์งˆ๋ฌธ์— ๋Œ€ํ•œ ๋‹ต๋ณ€์ž…๋‹ˆ๋‹ค.")
public String answer;
}
Loading