## ChatGPT 클라이언트
### ChatGPT API 토큰 환경 변수 등록
```powershell
# 관리자 권한으로 실행
setx CHATGPT_API_TOKEN "[토큰]"
refreshenv
```
- 토큰을 하드 코딩해서 입력하면 **안** 된다. Git을 통해 협업할 때 따로 무시하도록 설정도 해야 하고, 누군가 지나가다가 내 화면을 보는 순간 토큰을 알 수 있기 때문이다.
- 좋은 대안 중 하나는 환경 변수로 토큰 값을 저장하고 런타임에서 토큰 값을 가져 오는 것이다.
- ChatGPT API 토큰 발급 방법은 [여기](https://rfriend.tistory.com/794)에 잘 정리되어 있다.
### Config 추가
#### ChatGPTConfig
```java
public class ChatGPTConfig {
static public final String MODEL = "gpt-3.5-turbo";
static public final String URL = "https://api.openai.com/v1/chat/completions";
static public final float TEMPERATURE = 0.7F;
static public final String ROLE = "user";
}
```
- ChatGPT API로 요청을 보낼 때 설정할 값들이 많다. 그런 값들을 일괄적으로 관리해야 나중에 수정하기 편할 것이다. 그러니 따로 `ChatGPTConfig`를 만들어 관리하자.
#### ChatGPTClientConfig
```java
@Configuration
public class ChatGPTClientConfig {
@Bean
public WebClient webClient(){
return WebClient.builder()
.baseUrl(ChatGPTConfig.URL)
.defaultHeaders(
httpHeaders -> {
httpHeaders.add("Authorization", "Bearer " + System.getenv("CHATGPT_API_TOKEN"));
httpHeaders.add("Content-Type", "application/json");
}
).build();
}
}
```
- ChatGPT에 요청을 보낼 때 토큰이나 url 등의 설정 값은 모두 같다. 그렇다면 굳이 매번 `WebClient` 인스턴스를 만들 이유가 없다. 그러니 빈으로 등록하고 주입 받아 사용하자.
### DTO
#### ChatGPTRequest
##### ChatGPT API 요청 바디 예시
```json
{
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "Say this is a test!"}],
"temperature": 0.7
}
```
- `ChatGPTRequest`는 ChatGPT API 요청 바디를 자바 클래스로 만든 것이다. 따라서 코드를 구현하기 전에 미리 ChatGPT API 요청 바디의 구조를 파악해야 한다. 위는 ChatGPT API 요청 바디의 예시다.
- `messages` 이외에는 따로 특이한 건 없다. `messages`는 리스트 안에 object 형식으로 `role`과 `content` 값을 받는다.
##### 코드
```java
@Getter
@EqualsAndHashCode
@ToString
public class ChatGPTRequest {
final private String model = ChatGPTConfig.MODEL;
final private float temperature = ChatGPTConfig.TEMPERATURE;
@Getter
@ToString
@EqualsAndHashCode
@AllArgsConstructor
static
class ChatGPTMessage{
private String role;
private String content;
}
private final List<ChatGPTMessage> messages = new ArrayList<>();
public ChatGPTRequest(String content){
messages.add(new ChatGPTMessage(ChatGPTConfig.ROLE, content));
}
}
```
- `ChatGPTMessage`는 위 예시 JSON에서 `{"role": "user", "content": "Say this is a test!"}` 와 같은 Object에 해당한다. ChatGPT API에 요청을 보낼 때 이외에는 사용할 일이 없는 클래스이니 내부 클래스로 선언한다.
### Client
#### ChatGPTClient
##### ChatGPT API 응답 바디 예시
```json
{
"id": "chatcmpl-XXX",
"object": "chat.completion",
"created": XXX,
"model": "gpt-3.5-turbo-0613",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "This is a test!" // 이게 답변이다.
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 13,
"completion_tokens": 5,
"total_tokens": 18
},
"system_fingerprint": null
}
```
- `ChatGPTClient`는 요청을 보낸 다음 응답 바디에서 ChatGPT의 답변만 추출해야 하므로, ChatGPT API 응답 바디의 구조를 파악해야 한다. 위는 ChatGPT API 응답 바디의 예시다.
- `choices`는 리스트기 때문에 `choices` > `message`가 아니라 `choices` > `0` > `message` > `content`로 접근해야 한다는 점을 주의하자.
##### 클래스 생성 및 의존성 주입
```java
@Component
public class ChatGPTClient {
@Autowired
private WebClient webClient;
@Autowired
private PromptTemplateService service;
}
```
- [[7. Spring WebClient를 통한 Client 구현#ChatGPTClientConfig|WebClient]]처럼 `ChatGPTClient`도 인스턴스를 매번 생성할 이유가 없으므로 빈으로 등록한다.
- `PromptTemplateService`은 템플릿을 통한 프롬프트 생성 기능을 사용하기 위해 주입한다.
- 컨트롤러가 아닌데 서비스를 주입하는 게 바람직한지 고민해봤다. 내 생각은 '괜찮다'이다. 왜냐하면 `ChatGPTClient` 는 컨트롤러에서만 호출하기 때문에 컨트롤러와 같은 **인터페이스 레이어**에 있기 때문이다.
##### `chat`
```java
public String chat(String content) throws JsonProcessingException {
return webClient.post()
.bodyValue(new ChatGPTRequest(content))
.retrieve()
.bodyToMono(JsonNode.class)
.map(
jsonNode -> jsonNode
.path("choices")
.path(0)
.path("message")
.path("content")
.asText()
)
.block();
}
```
- API 요청을 보내고 `jackon`의 `JsonNode`를 통해 응답 속 `content`를 파싱하여 문자열로 반환하는 메서드다. `WebClient` 사용법은 [여기](https://gngsn.tistory.com/154)에 잘 정리되어있다.
##### `chatByTemplate`
```java
public String chatByTemplate(String templateId, Map<String, String> map)
throws TemplateException, IOException {
return chat(service.generatePromptByTemplate(templateId, map));
}
```
- `PromptTemplateService`를 이용하여 템플릿으로 프롬프트를 생성한 다음 API 요청을 보내는 메서드다.
## [[4-2. 클라이언트 테스트 코드|테스트 코드]]
![[4-2. 클라이언트 테스트 코드#`ChatGPTClientTest` 작성]]