openai연동 및 ai 서비스 생성

This commit is contained in:
2025-04-25 15:58:07 +09:00
parent d7c6ce3277
commit 45e6b05cab
10 changed files with 635 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
package com.caliverse.admin.domain.api;
import com.caliverse.admin.domain.request.AIRequest;
import com.caliverse.admin.domain.response.AIResponse;
import com.caliverse.admin.domain.service.AIService;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
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;
@Tag(name = "AI 관련", description = "AI api 입니다.")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/ai")
public class AIController {
private final AIService aiService;
@PostMapping("/analyze")
public ResponseEntity<AIResponse> aiAnalyze(@RequestBody AIRequest dataRequest){
return ResponseEntity.ok().body(aiService.aiMessageAnalyze(dataRequest));
}
}

View File

@@ -0,0 +1,6 @@
package com.caliverse.admin.domain.entity.AI;
public enum AIRequestType {
BUSINESS_LOG,
MAIL
}

View File

@@ -0,0 +1,7 @@
package com.caliverse.admin.domain.entity.AI;
public enum AIRole {
user,
system,
assistant
}

View File

@@ -0,0 +1,19 @@
package com.caliverse.admin.domain.request;
import com.caliverse.admin.domain.entity.AI.AIRequestType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AIRequest {
private String message;
private AIRequestType type;
private Map<String, Object> conditions;
}

View File

@@ -0,0 +1,20 @@
package com.caliverse.admin.domain.request;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class OpenAIRequest {
private String model;
private List<Map<String, String>> messages;
@Builder.Default
private double temperature = 0.7;
}

View File

@@ -0,0 +1,40 @@
package com.caliverse.admin.domain.response;
import com.caliverse.admin.history.domain.DynamodbDataInitializeHistory;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AIResponse {
private int status;
private String result;
@JsonProperty("data")
private ResultData resultData;
@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class ResultData {
private String message;
private String result;
private int total;
@JsonProperty("total_all")
private int totalAll;
@JsonProperty("page_no")
private int pageNo;
}
}

View File

@@ -0,0 +1,33 @@
package com.caliverse.admin.domain.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class OpenAIResponse {
private List<Choice> choices;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class Choice {
private Message message;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class Message {
private String role;
private String content;
}
}

View File

@@ -0,0 +1,110 @@
package com.caliverse.admin.domain.service;
import com.caliverse.admin.domain.dao.admin.MailMapper;
import com.caliverse.admin.domain.entity.AI.AIRequestType;
import com.caliverse.admin.domain.entity.AI.AIRole;
import com.caliverse.admin.domain.entity.Mail;
import com.caliverse.admin.domain.request.AIRequest;
import com.caliverse.admin.domain.request.LogGenericRequest;
import com.caliverse.admin.domain.request.OpenAIRequest;
import com.caliverse.admin.domain.response.AIResponse;
import com.caliverse.admin.domain.response.OpenAIResponse;
import com.caliverse.admin.global.common.code.CommonCode;
import com.caliverse.admin.global.common.constants.CommonConstants;
import com.caliverse.admin.global.common.utils.CommonUtils;
import com.caliverse.admin.logs.Indicatordomain.GenericMongoLog;
import com.caliverse.admin.logs.logservice.businesslogservice.BusinessLogGenericService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
public class AIService {
@Autowired
private ObjectMapper mapper;
private final OpenAIService openAIService;
private final BusinessLogGenericService businessLogGenericService;
private final MailMapper mailMapper;
public AIResponse aiMessageAnalyze(AIRequest dataRequest){
List<Map<String,String>> messages = initMessage();
List<?> data = getDataList(dataRequest.getType(), dataRequest.getConditions());
messages = splitDataToMessages(messages, data, dataRequest.getMessage());
OpenAIRequest openAIRequest = new OpenAIRequest();
openAIRequest.setMessages(messages);
OpenAIResponse result = openAIService.askOpenAI(openAIRequest);
log.info(result.getChoices().get(0).getMessage().getContent());
return AIResponse.builder()
.resultData(AIResponse.ResultData.builder()
.result(result.getChoices().get(0).getMessage().getContent())
.build())
.status(CommonCode.SUCCESS.getHttpStatus())
.result(CommonCode.SUCCESS.getResult())
.build();
}
private List<Map<String, String>> splitDataToMessages(List<Map<String,String>> messages, List<?> dataList, String userMessage) {
String dataJson;
try {
dataJson = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(dataList);
} catch (JsonProcessingException e) {
throw new RuntimeException("JSON 변환 실패", e);
}
messages.add(CommonUtils.getAIMessage(AIRole.user, messageMerge(userMessage, dataJson)));
return messages;
}
private String messageMerge(String message, String data){
return String.format("사용자 질문: %s\n\n아래는 시스템이 제공하는 데이터입니다.\n%s", message, data);
}
private List<Map<String,String>> initMessage(){
List<Map<String,String>> messages = new ArrayList<>();
messages.add(CommonUtils.getAIMessage(AIRole.system,
"""
너는 프론트엔드 게시용 데이터를 생성하는 게임 데이터 분석 AI야. \
사용자는 게임 로그 데이터와 함께 다양한 질문을 보낼 수 있어. \
너는 항상 HTML 또는 React 코드 형식으로 응답해야 해. \
응답에는 반드시 다음 항목을 포함해줘: \
1) 분석된 내용 설명 \
2) 분석된 요약 결과 (표 또는 문단) \
3) 시각화 차트 (예: bar chart, pie chart 등) \
응답은 반드시 한국어로 작성해. \
절대로 파이썬, Java, Node.js 코드 같은 것은 포함하지 마. \
사용자의 웹에 그대로 게시될 수 있는 코드만 생성해. \
React를 쓸 경우, React 18 기준 코드로 TailwindCSS 기반의 스타일을 사용해줘.
"""));
return messages;
}
private List<?> getDataList(AIRequestType type, Map<String, Object> conditions){
switch (type){
case BUSINESS_LOG -> {
LogGenericRequest logReq = mapper.convertValue(conditions, LogGenericRequest.class);
List<GenericMongoLog> logs = businessLogGenericService.loadBusinessLogData(logReq, GenericMongoLog.class, true);
return logs;
}
case MAIL -> {
List<Mail> mailList = mailMapper.getMailList(conditions);
return mailList;
}
default -> throw new RuntimeException("Not Type");
}
}
}

View File

@@ -0,0 +1,358 @@
package com.caliverse.admin.domain.service;
import com.caliverse.admin.domain.request.OpenAIRequest;
import com.caliverse.admin.domain.response.OpenAIResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.*;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RequiredArgsConstructor
@Service
@Slf4j
public class OpenAIService {
private final RestTemplate restTemplate;
@Value("${open-ai.url}")
private String api_url;
@Value("${open-ai.key}")
private String api_key;
@Value("${open-ai.chatModel}")
private String api_chat_model;
@Value("${open-ai.assistantsModel}")
private String api_assistants_model;
public OpenAIResponse askOpenAI(OpenAIRequest request) {
request.setModel(api_chat_model);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(api_key);
HttpEntity<OpenAIRequest> entity = new HttpEntity<>(request, headers);
ResponseEntity<OpenAIResponse> response = restTemplate.exchange(
api_url + "chat/completions",
HttpMethod.POST,
entity,
OpenAIResponse.class
);
return response.getBody();
}
// 파일 업로드 메서드
public String uploadFile(String filePath) {
String url = "https://api.openai.com/v1/files";
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(api_key);
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", new FileSystemResource(new File(filePath)));
body.add("purpose", "assistants");
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
Map.class
);
log.info("File uploaded successfully: {}", response.getBody());
return (String) response.getBody().get("id");
}
// Assistant 생성 메서드
public String createAssistant(String name, String instructions) {
String url = "https://api.openai.com/v1/assistants";
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(api_key);
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> body = new HashMap<>();
body.put("model", api_assistants_model);
body.put("name", name);
body.put("instructions", instructions);
body.put("tools", List.of(Map.of("type", "retrieval")));
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
Map.class
);
log.info("Assistant created successfully: {}", response.getBody());
return (String) response.getBody().get("id");
}
// Thread 생성 메서드
public String createThread() {
String url = "https://api.openai.com/v1/threads";
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(api_key);
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> body = new HashMap<>();
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
Map.class
);
log.info("Thread created successfully: {}", response.getBody());
return (String) response.getBody().get("id");
}
// Thread에 메시지 추가 메서드
public String addMessageToThread(String threadId, String content, String fileId) {
String url = "https://api.openai.com/v1/threads/" + threadId + "/messages";
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(api_key);
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> body = new HashMap<>();
body.put("role", "user");
body.put("content", content);
if (fileId != null && !fileId.isEmpty()) {
body.put("file_ids", Collections.singletonList(fileId));
}
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
Map.class
);
log.info("Message added to thread successfully: {}", response.getBody());
return (String) response.getBody().get("id");
}
// Run 실행 메서드
public String startRun(String threadId, String assistantId) {
String url = "https://api.openai.com/v1/threads/" + threadId + "/runs";
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(api_key);
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> body = new HashMap<>();
body.put("assistant_id", assistantId);
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
Map.class
);
log.info("Run started successfully: {}", response.getBody());
return (String) response.getBody().get("id");
}
// Run 상태 확인 메서드
public Map<String, Object> checkRunStatus(String threadId, String runId) {
String url = "https://api.openai.com/v1/threads/" + threadId + "/runs/" + runId;
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(api_key);
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
ResponseEntity<Map> response = restTemplate.exchange(
url,
HttpMethod.GET,
requestEntity,
Map.class
);
log.debug("Run status: {}", response.getBody());
return response.getBody();
}
// Thread의 메시지 목록 조회 메서드
public List<Map<String, Object>> listMessages(String threadId) {
String url = "https://api.openai.com/v1/threads/" + threadId + "/messages";
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(api_key);
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
ResponseEntity<Map> response = restTemplate.exchange(
url,
HttpMethod.GET,
requestEntity,
Map.class
);
log.debug("Thread messages: {}", response.getBody());
return (List<Map<String, Object>>) response.getBody().get("data");
}
// 마지막 Assistant 메시지 추출 메서드
public String extractLastAssistantMessage(String threadId) {
List<Map<String, Object>> messages = listMessages(threadId);
for (Map<String, Object> message : messages) {
if ("assistant".equals(message.get("role"))) {
List<Map<String, Object>> content = (List<Map<String, Object>>) message.get("content");
if (content != null && !content.isEmpty()) {
for (Map<String, Object> contentItem : content) {
if ("text".equals(contentItem.get("type"))) {
Map<String, Object> text = (Map<String, Object>) contentItem.get("text");
return (String) text.get("value");
}
}
}
break; // 가장 최신 assistant 메시지만 처리
}
}
return "No assistant response found";
}
// 파일 삭제 메서드
public boolean deleteFile(String fileId) {
String url = "https://api.openai.com/v1/files/" + fileId;
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(api_key);
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
try {
ResponseEntity<Map> response = restTemplate.exchange(
url,
HttpMethod.DELETE,
requestEntity,
Map.class
);
log.info("File deleted successfully: {}", response.getBody());
return true;
} catch (Exception e) {
log.error("Error deleting file: {}", e.getMessage());
return false;
}
}
// Assistant 가져오기 또는 생성 메서드
public String getOrCreateAssistant() {
String url = "https://api.openai.com/v1/assistants";
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(api_key);
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
ResponseEntity<Map> response = restTemplate.exchange(
url,
HttpMethod.GET,
requestEntity,
Map.class
);
List<Map<String, Object>> assistants = (List<Map<String, Object>>) response.getBody().get("data");
// 데이터 분석 어시스턴트 찾기
for (Map<String, Object> assistant : assistants) {
if ("Data Analyzer".equals(assistant.get("name"))) {
return (String) assistant.get("id");
}
}
// 없으면 새로 생성
return createAssistant("Data Analyzer",
"You are a data analysis assistant. Analyze the provided JSON data and generate visualizations. " +
"Your responses should be in HTML format with appropriate visualizations using charts. " +
"Focus on identifying patterns, trends, and insights from the data. " +
"Use Korean language for all responses.");
}
// 전체 비동기 프로세스 실행 및 결과 대기 메서드
public String processDataAnalysisAndWait(String filePath, String question) {
try {
// 1. 파일 업로드
String fileId = uploadFile(filePath);
// 2. Assistant 얻기 또는 생성
String assistantId = getOrCreateAssistant();
// 3. Thread 생성
String threadId = createThread();
// 4. 메시지 추가
addMessageToThread(threadId, question, fileId);
// 5. Run 실행
String runId = startRun(threadId, assistantId);
// 6. 완료 대기 (최대 60초, 3초마다 체크)
String status = "queued";
int maxAttempts = 20;
int attempts = 0;
while (!status.equals("completed") && attempts < maxAttempts) {
Thread.sleep(3000);
Map<String, Object> runStatus = checkRunStatus(threadId, runId);
status = (String) runStatus.get("status");
if (status.equals("failed") || status.equals("cancelled")) {
throw new RuntimeException("Run failed with status: " + status);
}
attempts++;
}
if (!status.equals("completed")) {
throw new RuntimeException("Run timed out after 60 seconds");
}
// 7. 응답 메시지 추출
String result = extractLastAssistantMessage(threadId);
// 8. 파일 정리 (선택적)
deleteFile(fileId);
return result;
} catch (Exception e) {
log.error("Error in data analysis process: {}", e.getMessage());
throw new RuntimeException("Data analysis failed: " + e.getMessage(), e);
}
}
}

View File

@@ -1,4 +1,8 @@
spring:
servlet:
multipart:
max-file-size: 5MB
max-request-size: 10MB
## deploy
# profiles:
# active: stage
@@ -25,4 +29,15 @@ spring:
password:
expiration-days: 180
open-ai:
key: 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
url: https://api.openai.com/v1/
chatModel: gpt-4o
assistantsModel: gpt-4-1106-preview
claude:
key: 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
model: claude-3-7-sonnet-20250219
url: https://api.anthropic.com/v1/messages