개인프로젝트/기능프로그램_오늘뭐입지

20240513_ChatGpt Api 사용하기

일일일코_장민기 2024. 5. 13. 07:14
728x90

질문과 답변 출력

 

 

 

내일 일이 생겨서 작업 시간이 부족할 것 같아, 오늘 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해도 되긴 할 것이다.