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 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; } }