refs #739: normalize MCP query plan envelopes

This commit is contained in:
devmrko
2026-07-28 10:31:20 +09:00
parent d13d6d48ca
commit 6d98931ce1

View File

@@ -2,6 +2,7 @@ package com.cloudhandson.vpdbackoffice.service;
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
import com.cloudhandson.vpdbackoffice.domain.token.BearerTokenRecord;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
@@ -185,14 +186,16 @@ public class SelectAiService {
}
private QueryPlanContext requiredQueryPlan(JsonNode plan) {
String targetType = plan.path("targetType").asText("").trim().toUpperCase(Locale.ROOT);
JsonNode normalizedPlan = unwrapMcpToolResult(plan);
String targetType = normalizedPlan.path("targetType")
.asText("").trim().toUpperCase(Locale.ROOT);
if (!Set.of("NONE", "SINGLE", "MULTI", "ALL").contains(targetType)
|| !plan.path("targets").isArray()) {
|| !normalizedPlan.path("targets").isArray()) {
throw new AppException(
"queryPlan은 targetType(NONE/SINGLE/MULTI/ALL)과 targets 배열이 필요합니다.");
}
Set<String> allowedPrefixes = new LinkedHashSet<>();
for (JsonNode target : plan.path("targets")) {
for (JsonNode target : normalizedPlan.path("targets")) {
String prefix = target.path("gamePrefix").asText("").trim();
if (!prefix.isEmpty()) {
allowedPrefixes.add(prefix.toUpperCase(Locale.ROOT));
@@ -200,13 +203,40 @@ public class SelectAiService {
}
return new QueryPlanContext(
targetType,
plan.path("status").asText(""),
plan.path("targets").size(),
normalizedPlan.path("status").asText(""),
normalizedPlan.path("targets").size(),
Set.copyOf(allowedPrefixes),
plan.deepCopy()
normalizedPlan.deepCopy()
);
}
/**
* Accept the direct game-query-plan contract as well as the standard MCP
* tools/call envelope the ReAct model can copy from a preceding observation.
* This is transport normalization only; the target contract itself remains
* the authoritative data-driven policy.
*/
private JsonNode unwrapMcpToolResult(JsonNode candidate) {
JsonNode current = candidate;
for (int depth = 0; depth < 3 && current != null; depth++) {
if (current.isTextual()) {
try {
current = objectMapper.readTree(current.asText());
continue;
} catch (JsonProcessingException ignored) {
return current;
}
}
JsonNode response = current.path("response");
if (response.isObject() || response.isTextual()) {
current = response;
continue;
}
break;
}
return current == null ? objectMapper.createObjectNode() : current;
}
private GameContext resolveGameContext(String bearerToken, String question) {
try {
List<GameCatalogVectorService.GameCandidate> candidates =