내일 일이 생겨서 작업 시간이 부족할 것 같아, 오늘 Chat Gpt 작업에 들어갔다.
기능은 간단하게 GPT에 질문하고 답변을 받아오는 기능이다.
나중에 오늘의 날씨와 미세먼지 데이터 + 옷 데이터를 넣어서 뭘 입으면 좋을지 답변을 듣기 위한 기능이다.
전체 코드
DB
create table gptAnswervo(
id serial primary key,
username varchar(50) not null,
question VARCHAR(50000) not null,
answer VARCHAR(50000) not null
)
application.properties
openai.secret-key=**
컨트롤러
package org.gpt.ask;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.gpt.GptanswervoDto;
import org.gpt.saveAnswer.SaveAnswerService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@RequiredArgsConstructor
@Slf4j
@Controller
public class GptController {
private final GptService gptService;
private final SaveAnswerService saveAnswerService;
@ResponseBody
@PostMapping("/askToGpt")
public String askToGpt(String question) throws JsonProcessingException {
String username = "qwe";
String answer = gptService.getGptResponse(question);
GptanswervoDto gptanswervoDto = new GptanswervoDto(username, question, answer);
saveAnswerService.saveAnswer(gptanswervoDto);
return answer;
}
@GetMapping("/gptMain")
public String gptMain() {
return "GptMain";
}
}
GPT서비스
package org.gpt.saveAnswer;
import lombok.RequiredArgsConstructor;
import org.gpt.Gptanswervo;
import org.gpt.GptanswervoDto;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class SaveAnswerService {
public final SaveAnswerRepository saveAnswerRepository;
public void saveAnswer(GptanswervoDto gptanswervoDto) {
Gptanswervo gptanswervo = new Gptanswervo();
BeanUtils.copyProperties(gptanswervoDto, gptanswervo);
saveAnswerRepository.save(gptanswervo);
}
}
Save서비스
package org.gpt.saveAnswer;
import lombok.RequiredArgsConstructor;
import org.gpt.Gptanswervo;
import org.gpt.GptanswervoDto;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class SaveAnswerService {
public final SaveAnswerRepository saveAnswerRepository;
public void saveAnswer(GptanswervoDto gptanswervoDto) {
Gptanswervo gptanswervo = new Gptanswervo();
BeanUtils.copyProperties(gptanswervoDto, gptanswervo);
saveAnswerRepository.save(gptanswervo);
}
}
리포지토리
package org.gpt.saveAnswer;
import org.gpt.Gptanswervo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SaveAnswerRepository extends JpaRepository<Gptanswervo, Integer> {
}
질문용 DTO
package org.gpt;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.Value;
import java.io.Serializable;
/**
* model: 사용할 모델
* prompt: 질문
* temperature: 창의성(1일수록 높음)
* max_tokens: 최대 사용 토큰
*/
@Data
@Value
public class GptQuestionDto implements Serializable {
@NotNull
@Size(max = 50)
String model = "gpt-3.5-turbo";
@NotNull
@Size(max = 50000)
String prompt;
@NotNull
float temperature = 1.0F;
@NotNull
int max_tokens = 200;
}
답변 저장용 VO
package org.gpt;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.ColumnDefault;
@Getter
@Setter
@Entity
@Table(name = "gptanswervo")
public class Gptanswervo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ColumnDefault("nextval('gptanswervo_id_seq'")
@Column(name = "id", nullable = false)
private Integer id;
@Size(max = 50)
@NotNull
@Column(name = "username", nullable = false, length = 50)
private String username;
@Size(max = 50000)
@NotNull
@Column(name = "question", nullable = false, length = 50000)
private String question;
@Size(max = 50000)
@NotNull
@Column(name = "answer", nullable = false, length = 50000)
private String answer;
}
답변 저장용 DTO
package org.gpt;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.Value;
import java.io.Serializable;
/**
* DTO for {@link Gptanswervo}
*/
@Data
@Value
public class GptanswervoDto implements Serializable {
@NotNull
@Size(max = 50)
String username;
@NotNull
@Size(max = 50000)
String question;
@NotNull
@Size(max = 50000)
String answer;
}
jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script type="text/javascript" src="<c:url value='/resources/js/GptMain.js'/>"></script>
</head>
<body>
<h1>Gpt Main Page</h1>
<form id="DustRequest" action="<c:url value='/askToGpt'/>" method="POST">
<input type="text" id="question" name="question">
<input type="submit" value="확인">
</form>
<span id="gptAnswer"></span>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
crossorigin="anonymous"></script>
</body>
</html>
js
$(function(){
//Gpt 답변 출력 ajax
$("#DustRequest").on("submit", function(event){
event.preventDefault();
var question = $("#question").val()
var gptAnswer = $("#gptAnswer")
$.ajax({
type: "POST",
url: "/askToGpt",
data: {question: question},
datatype: "text",
success: function(response){
gptAnswer.text(response);
},
error: function(){
console.log("Gpt 답변 출력 에러")
}
})
})
})//$(function()
기능은 단순하지만 쓰기는 복잡하다;;
컨트롤러
public String askToGpt(String question) throws JsonProcessingException {
String username = "qwe";
String answer = gptService.getGptResponse(question);
GptanswervoDto gptanswervoDto = new GptanswervoDto(username, question, answer);
saveAnswerService.saveAnswer(gptanswervoDto);
return answer;
}
- jsp에서 질문(question)을 입력하면 유저 명과 함께 컨트롤러로 들어온다(수정될 예정)
- 질문을 GPT에게 넘긴다
- 받은 답변과 유저명, 질문을 DB에 넣는다
- jsp로 답변을 출력한다.
GPT 서비스
- 공부하면서 메모한 것들이다. 간단한 것들이라도 용어를 명확하게 이해하기 위해 기입했다.
1. `String url = "https://api.openai.com/v1/chat/completions";`: 요청을 보낼 엔드포인트 URL을 지정합니다.
- 엔드포인트 url: 요청을 보낼 url
- 같은 url이라도 HTTP 메소드에 따라 다른 행위를 요청할 수 있게끔 구별해주는 항목
- 정보를 주고 받는 길
2. `HttpHeaders headers = new HttpHeaders();`: HTTP 요청 헤더를 생성합니다.
- HTTP 요청 헤더: 클라이언트가 서버에 요청을 보낼 때 함께 보내는 정보
- 요청의 내용이나 요청한 데이터의 형식을 서버에게 알려주거나, 요청을 보내는 클라이언트가 누구인지 인증하는 역할
- 해당 정보들은 서버에 도착하면 서버가 요청을 올바르게 처리할 수 있도록 도움
3. `headers.setBearerAuth(secretKey);`: Bearer 토큰을 사용하여 요청에 인증 정보를 추가합니다.
- 헤더에 Bearer 토큰을 사용하여 클라이언트가 해당 서비스에 인증된 사용자임을 증명하는데 사용
4. `headers.add("Content-Type", "application/json");`: 요청 헤더에 JSON 형식의 콘텐츠 타입을 추가합니다.
- 서버에게 클라이언트가 JSON 형식으로 데이터를 보내기 위함
Map<String, Object> map = new HashMap<>();
map.put("model", questionDto.getModel());
map.put("temperature", temperature);
map.put("max_tokens", max_tokens);
5. `map.put("message", Collections.singletonList(Map.of("role", "user", "content", questionDto.getPrompt())));`: 요청 본문에 사용자의 메시지를 추가합니다.
- Key - value : role = user // content = questionDto.getPrompt()
- Collections: 데이터를 저장 / 조작 / 검색할 때 사용되는 클래스와 인터페이스를 제공하는 유틸리티 클래스
- List / Set / Map 등과 같은 데이터 구조를 다루는 메서드가 포함
ex) 리스트에 요소를 추가하거나 제거하는 메서드 / 세트에서 중복된 요소를 제거하는 메서드 / 맵에서 특정 키에 해당하는 값을 가져오는 메서드 등
- singletonList: 리스트 안에 하나의 요소만 들어있는 리스트를 만들 수 있으며, 만든 뒤에 안의 요소를 변경할 수 없음
- 메서드가 리스트 형태를 원할 경우에 사용
6. `String requestJson = objectMapper.writeValueAsString(map);`: 맵을 JSON 문자열로 변환합니다.
- `objectMapper`: - 자바 객체를 JSON 형식의 문자열로 변환하거나 JSON 문자열을 JAVA 객체로 변환할 때 사용(Jackson 라이브러리에서 제공)
- JSON 직렬화를 위해 사용됨(JSON 형식의 문자열로 변환)
- writeValueAsString(): java 객체를 JSON 형식의 문자열로 변환하는 역할
- readValue(): JSON 형식의 문자열을 java 객체로 역직렬화하는 역할
ex) String jsonString = "{\"name\": \"John\", \"age\": 30}";
Person person = objectMapper.readValue(jsonString, Person.class);
7. `HttpEntity<String> request = new HttpEntity<>(requestJson, headers);`: JSON 형식의 요청 본문과 헤더를 포함하는 HTTP 요청 엔터티를 생성합니다.
- HttpEntity: - HTTP 요청이나 응답의 본문을 포함하는 엔터티
- String: - 서버로 String 타입(JSON 형식의 문자열)로 보내겠다는 뜻
- requestJson: - String 타입으로 보낼 본문
- headers: - 요청을 보낼 때 함께 전송되는 추가 정보를 포함한 헤더
8. ResponseEntity<받을 타입> response = restTemplate.exchange(보낼 주소, HttpMethod.요청방법, HttpEntity객체, 받을 타입.class);
- ResponseEntity: - HTTP 응답을 나타내는 객체(응답 상태 코드, 헤더, 본문 등의 정보가 포함)
- String: - String 타입으로 받겠다는 뜻(대부분 JSON 형식의 문자열)
- restTemplate: - HTTP 통신을 위한 도구
- RESTful API 웹 서비스와의 상호작용을 쉽게 외부 도메인에서 데이터를 가져오거나 전송할 때 사용되는 스프링 프레임워크의 클래스
- exchange(): - RestTemplate 클래스의 메서드로 헤더를 생성하고 모든 HTTP 요청 방법을 허용
- ResponseEntity로 반환
- (보낼 주소, HttpMethod.요청 방법, HttpEntity객체, 받을 타입.class)
- postForEntity(): - post 타입의 exchange();
9. JsonNode node = objectMapper.readTree(response.getBody());
String content = node.path("choices").get(0).path("message").path("content").asText();
- JsonNode: - JSON 데이터를 트리 구조로 표현하는 노드(JSON 데이터 구조를 메모리에서 나타내는 자료 구조)(노드: 그래프의 구성 요소 중 하나로, 데이터를 담고 있고 다른 노드들과의 관계를 표현함)
- JSON 데이터의 구조를 표현하고, 데이터에 접근하고 조작할 때 사용
- path("choices"): 쉼표로 구분된 JSON 데이터 중 key가 choice인 value 선택
- get(0): value가 배열 형태이기 때문에 배열 중 첫번째 선택
- path("message"): 배열 내부의 JSON 데이터 중 key가 message인 value 선택
- path("content"): message의 value 내부의 JSON 데이터 중 key가 content인 value 선택
- asText(): key가 content인 value를 Text로 전환
- readTree: - JSON 문자열을 JsonNode 객체로 파싱
GPT 답변 예시(ResponseEntity<String> response를 출력한 결과)
<200 OK OK,
{
"id": "아이디",
"object": "chat.completion",
"created": ******,
"model": "gpt-3.5-turbo-0125",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello! How can I assist you today?"
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 8,
"completion_tokens": 9,
"total_tokens": 17
},
"system_fingerprint": null
}
,[Date:"Mon, 13 May 2024 09:16:23 GMT", 이하 생략]>

그 외의 코드는 별거 없으니 생략
출력
** openai.secret-key
openai.secret-key 는 git에 올라가지 않도록 되어 있다(보안 상의 문제)
openai.secret-key 를 지우고 올리거나, gitignore 처리를 하자
안 그러면 이렇게 된다.
++ 생각보다 간단히 해결되었다
remote: (?) To push, remove secret from commit(s) or follow this URL to allow the secret.
remote: https://github.com/JJMMKKK/whatShouldIWearToday/security/secret-scanning/unblock-secret/**
제공된 url로 들어가면 어떻게 처리할지 선택하는 페이지가 나온다(이걸 캡처해야되는데 깜빡했다)
- 여기서 잘못된 차단을 선택하면 이제 푸시할 수 있다고 된다
- 그런 다음 api Key를 빼고 다시 push하면 된다.
- 아니면 거절하고 빼고 push해도 되긴 할 것이다.
'개인프로젝트 > 기능프로그램_오늘뭐입지' 카테고리의 다른 글
20240514_날씨 모듈 통합 (1) | 2024.05.15 |
---|---|
20240514_미세먼지 파트 조정 (0) | 2024.05.14 |
20240512_미세먼지 데이터 (1) | 2024.05.12 |
오늘 뭐입지? 프로그램 관련 각종 문서 (0) | 2024.05.12 |
20240511_날씨 데이터 출력 (0) | 2024.05.12 |