131 lines
4.7 KiB
Java
131 lines
4.7 KiB
Java
package com.cloudhandson.vpdbackoffice.service;
|
|
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
import java.net.URI;
|
|
import java.util.UUID;
|
|
import org.springframework.beans.factory.annotation.Qualifier;
|
|
import org.springframework.http.HttpEntity;
|
|
import org.springframework.http.HttpHeaders;
|
|
import org.springframework.http.HttpMethod;
|
|
import org.springframework.http.MediaType;
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.web.client.HttpStatusCodeException;
|
|
import org.springframework.web.client.ResourceAccessException;
|
|
import org.springframework.web.client.RestTemplate;
|
|
import org.springframework.web.util.UriComponentsBuilder;
|
|
|
|
/** Calls the ORDS boundary; the database endpoint owns bearer-to-VPD-context mapping. */
|
|
@Service
|
|
public class SelectAiAgentOrdsService {
|
|
|
|
static final String ORDS_PATH = "/cb-ords/kb-select-ai-agent/run";
|
|
private static final int MAX_PROMPT_LENGTH = 8_000;
|
|
|
|
private final SettingService settingService;
|
|
private final RestTemplate restTemplate;
|
|
private final ObjectMapper objectMapper;
|
|
|
|
public SelectAiAgentOrdsService(
|
|
SettingService settingService,
|
|
@Qualifier("ordsAgentRestTemplate") RestTemplate restTemplate,
|
|
ObjectMapper objectMapper
|
|
) {
|
|
this.settingService = settingService;
|
|
this.restTemplate = restTemplate;
|
|
this.objectMapper = objectMapper;
|
|
}
|
|
|
|
public JsonNode run(String bearerToken, String prompt, String conversationId) {
|
|
String normalizedToken = required(bearerToken, "bearerToken");
|
|
String normalizedPrompt = required(prompt, "prompt");
|
|
if (normalizedPrompt.length() > MAX_PROMPT_LENGTH) {
|
|
throw new AppException("prompt는 " + MAX_PROMPT_LENGTH + "자 이하여야 합니다.");
|
|
}
|
|
String normalizedConversationId = normalizeConversationId(conversationId);
|
|
String baseUrl = settingService.ordsBaseUrl();
|
|
if (baseUrl == null || baseUrl.isBlank()) {
|
|
throw new AppException("ORDS base URL이 설정되지 않았습니다.");
|
|
}
|
|
|
|
ObjectNode requestBody = objectMapper.createObjectNode();
|
|
requestBody.put("prompt", normalizedPrompt);
|
|
if (normalizedConversationId != null) {
|
|
requestBody.put("conversationId", normalizedConversationId);
|
|
}
|
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
headers.setBearerAuth(normalizedToken);
|
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
|
headers.set("X-VPD-Probe-Id", UUID.randomUUID().toString());
|
|
|
|
try {
|
|
ResponseEntity<String> response = restTemplate.exchange(
|
|
endpoint(baseUrl),
|
|
HttpMethod.POST,
|
|
new HttpEntity<>(requestBody.toString(), headers),
|
|
String.class
|
|
);
|
|
JsonNode body = parse(response.getBody());
|
|
if (body.hasNonNull("error")) {
|
|
throw new AppException("Select AI Agent ORDS 오류: " + body.path("error").asText());
|
|
}
|
|
return body;
|
|
} catch (HttpStatusCodeException e) {
|
|
throw new AppException("Select AI Agent ORDS HTTP " + e.getStatusCode().value()
|
|
+ ": " + responseError(e.getResponseBodyAsString()));
|
|
} catch (ResourceAccessException e) {
|
|
throw new AppException("Select AI Agent ORDS 연결 또는 응답 시간 초과: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
private URI endpoint(String baseUrl) {
|
|
return UriComponentsBuilder.fromUriString(baseUrl)
|
|
.path(ORDS_PATH)
|
|
.build()
|
|
.toUri();
|
|
}
|
|
|
|
private JsonNode parse(String value) {
|
|
try {
|
|
if (value == null || value.isBlank()) {
|
|
throw new AppException("Select AI Agent ORDS 응답 본문이 비어 있습니다.");
|
|
}
|
|
return objectMapper.readTree(value);
|
|
} catch (AppException e) {
|
|
throw e;
|
|
} catch (Exception e) {
|
|
throw new AppException("Select AI Agent ORDS 응답 JSON 파싱 실패: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
private String responseError(String body) {
|
|
try {
|
|
JsonNode parsed = objectMapper.readTree(body);
|
|
return parsed.path("error").asText(body == null ? "" : body);
|
|
} catch (Exception ignored) {
|
|
return body == null ? "" : body;
|
|
}
|
|
}
|
|
|
|
private String required(String value, String name) {
|
|
if (value == null || value.isBlank()) {
|
|
throw new AppException(name + "은(는) 필수입니다.");
|
|
}
|
|
return value.trim();
|
|
}
|
|
|
|
private String normalizeConversationId(String value) {
|
|
if (value == null || value.isBlank()) {
|
|
return null;
|
|
}
|
|
String normalized = value.trim();
|
|
if (!normalized.matches("[A-Za-z0-9._:-]{1,128}")) {
|
|
throw new AppException("conversationId는 영문/숫자/._:-만 사용하고 128자 이하여야 합니다.");
|
|
}
|
|
return normalized;
|
|
}
|
|
}
|