441 lines
18 KiB
Java
441 lines
18 KiB
Java
package com.cloudhandson.vpdbackoffice.service;
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
|
|
import com.cloudhandson.vpdbackoffice.config.GameScopeProperties;
|
|
import com.cloudhandson.vpdbackoffice.config.McpProperties;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
class McpSseServiceTest {
|
|
|
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
private final SelectAiService selectAiService = new CapturingSelectAiService();
|
|
private final QaVectorService qaVectorService = new CapturingQaVectorService();
|
|
private final GameScopeService gameScopeService = new CapturingGameScopeService();
|
|
private final GameCatalogVectorService gameCatalogVectorService =
|
|
new CapturingGameCatalogVectorService();
|
|
private final McpSseService service = new McpSseService(
|
|
selectAiService,
|
|
objectMapper,
|
|
new BackofficeProperties(
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
new BackofficeProperties.SelectAi(
|
|
"", "", "", "SGMP_POC_OCI_GPT54MINI", true, 3)
|
|
),
|
|
new McpProperties(
|
|
"oracle.select_ai.test_data_text2sql",
|
|
"테스트 데이터 Text2SQL",
|
|
"테스트 데이터용 읽기 전용 SQL을 생성하고 실행합니다.",
|
|
"테스트 데이터의 조회 내용을 입력합니다.",
|
|
"oracle.select_ai.test_data_showprompt",
|
|
"테스트 데이터 SHOWPROMPT",
|
|
"테스트 데이터의 Select AI prompt를 조회합니다.",
|
|
"oracle.select_ai.test_qa_vector_search",
|
|
"테스트 예제 SQL 조회",
|
|
"테스트 질문에 사용할 예제 SQL을 조회합니다.",
|
|
"oracle.select_ai.test_qa_vector_store",
|
|
"테스트 예제 SQL 저장",
|
|
"테스트 예제 SQL을 저장합니다.",
|
|
"oracle.select_ai.test_fewshot_nl2sql",
|
|
"테스트 Few-shot NL2SQL",
|
|
"테스트 Few-shot 검색 및 실행 도구입니다.",
|
|
"oracle.select_ai.test_game_scope_resolve",
|
|
"테스트 게임 범위 확인",
|
|
"테스트 DB 게임 범위 확인 도구입니다."
|
|
),
|
|
qaVectorService,
|
|
gameScopeService,
|
|
gameCatalogVectorService
|
|
);
|
|
|
|
@Test
|
|
void listsQueryShowpromptAndQaVectorToolsWithTheirInputs() {
|
|
ObjectNode response = service.handle("default", request(1, "tools/list"));
|
|
|
|
var tools = response.path("result").path("tools");
|
|
assertThat(tools).hasSize(8);
|
|
var selectAi = tools.get(0);
|
|
assertThat(selectAi.path("name").asText()).isEqualTo("oracle.select_ai.test_data_text2sql");
|
|
assertThat(selectAi.path("description").asText()).contains("SGMP_POC_OCI_GPT54MINI");
|
|
assertThat(selectAi.path("inputSchema").path("required"))
|
|
.extracting(node -> node.asText())
|
|
.contains("prompt");
|
|
assertThat(selectAi.path("inputSchema").path("properties").has("bearerToken")).isFalse();
|
|
assertThat(selectAi.path("inputSchema").path("properties").has("limit")).isFalse();
|
|
assertThat(selectAi.path("inputSchema").path("properties").has("conversationId")).isFalse();
|
|
|
|
var showprompt = tools.get(1);
|
|
assertThat(showprompt.path("name").asText())
|
|
.isEqualTo("oracle.select_ai.test_data_showprompt");
|
|
assertThat(showprompt.path("description").asText())
|
|
.contains("SGMP_POC_OCI_GPT54MINI")
|
|
.contains("Select AI prompt");
|
|
assertThat(showprompt.path("inputSchema").path("required"))
|
|
.extracting(node -> node.asText())
|
|
.contains("prompt");
|
|
|
|
var vectorSearch = tools.get(2);
|
|
assertThat(vectorSearch.path("name").asText())
|
|
.isEqualTo("oracle.select_ai.test_qa_vector_search");
|
|
assertThat(vectorSearch.path("inputSchema").path("properties").path("question").path("type").asText())
|
|
.isEqualTo("string");
|
|
assertThat(vectorSearch.path("inputSchema").path("properties").path("topK").path("default").asInt())
|
|
.isEqualTo(3);
|
|
|
|
var vectorStore = tools.get(3);
|
|
assertThat(vectorStore.path("name").asText())
|
|
.isEqualTo("oracle.select_ai.test_qa_vector_store");
|
|
assertThat(vectorStore.path("inputSchema").path("required"))
|
|
.extracting(node -> node.asText())
|
|
.contains("question", "answerSql");
|
|
|
|
var fewShot = tools.get(4);
|
|
assertThat(fewShot.path("name").asText())
|
|
.isEqualTo("oracle.select_ai.test_fewshot_nl2sql");
|
|
assertThat(fewShot.path("description").asText())
|
|
.contains("Few-shot")
|
|
.contains("NONE, SINGLE, MULTI, ALL");
|
|
assertThat(fewShot.path("inputSchema").path("properties")
|
|
.path("queryPlan").path("description").asText())
|
|
.contains("NONE, SINGLE, MULTI, ALL");
|
|
|
|
var gameScope = tools.get(5);
|
|
assertThat(gameScope.path("name").asText())
|
|
.isEqualTo("oracle.select_ai.test_game_scope_resolve");
|
|
assertThat(gameScope.path("inputSchema").path("required"))
|
|
.extracting(node -> node.asText()).contains("question");
|
|
|
|
assertThat(tools.get(6).path("name").asText())
|
|
.isEqualTo("oracle.select_ai.game_catalog_resolve");
|
|
assertThat(tools.get(7).path("name").asText())
|
|
.isEqualTo("oracle.select_ai.game_query_plan");
|
|
assertThat(tools.get(7).path("description").asText())
|
|
.contains("항상 먼저 호출")
|
|
.contains("NONE, SINGLE, MULTI, ALL")
|
|
.contains("smilegate_fewshot_nl2sql");
|
|
}
|
|
|
|
@Test
|
|
void callsShowpromptWithoutExecutingGeneratedSql() {
|
|
ObjectNode request = request(4, "tools/call");
|
|
ObjectNode params = (ObjectNode) request.putObject("params");
|
|
params.put("name", "oracle.select_ai.test_data_showprompt");
|
|
params.putObject("arguments").put("prompt", "버블리즈 유저 마스터 조회해줘.");
|
|
|
|
ObjectNode response = service.handle("default", request, "user-bearer");
|
|
|
|
CapturingSelectAiService agentService = (CapturingSelectAiService) selectAiService;
|
|
assertThat(agentService.bearerToken).isEqualTo("user-bearer");
|
|
assertThat(agentService.prompt).isEqualTo("버블리즈 유저 마스터 조회해줘.");
|
|
assertThat(agentService.showpromptCalled).isTrue();
|
|
assertThat(response.path("result").path("isError").asBoolean()).isFalse();
|
|
assertThat(response.path("result").path("content").get(0).path("text").asText())
|
|
.contains("oracle.select_ai.test_data_showprompt")
|
|
.contains("SHOWPROMPT")
|
|
.contains("assembled Select AI prompt");
|
|
}
|
|
|
|
@Test
|
|
void callsVpdSelectAiThroughOrdsService() {
|
|
ObjectNode request = request(2, "tools/call");
|
|
ObjectNode params = (ObjectNode) request.putObject("params");
|
|
params.put("name", "oracle.select_ai.test_data_text2sql");
|
|
ObjectNode arguments = params.putObject("arguments");
|
|
arguments.put("prompt", "카제나 AU를 조회해 줘");
|
|
|
|
ObjectNode response = service.handle("default", request, "user-bearer");
|
|
|
|
CapturingSelectAiService agentService = (CapturingSelectAiService) selectAiService;
|
|
assertThat(agentService.bearerToken).isEqualTo("user-bearer");
|
|
assertThat(agentService.prompt).isEqualTo("카제나 AU를 조회해 줘");
|
|
assertThat(response.path("error").isMissingNode()).isTrue();
|
|
assertThat(response.path("result").path("isError").asBoolean()).isFalse();
|
|
String payload = response.path("result").path("content").get(0).path("text").asText();
|
|
assertThat(payload)
|
|
.contains("SGMP_POC_OCI_GPT54MINI")
|
|
.contains("SELECT 1 FROM DUAL")
|
|
.contains("SHOWSQL_AND_EXECUTED")
|
|
.contains("READ_ONLY_EXECUTED")
|
|
.contains("RESULT");
|
|
}
|
|
|
|
@Test
|
|
void callsDedicatedFewShotNl2SqlToolWithGeneratedSqlAndExecutionResult() {
|
|
ObjectNode request = request(7, "tools/call");
|
|
ObjectNode params = (ObjectNode) request.putObject("params");
|
|
params.put("name", "oracle.select_ai.test_fewshot_nl2sql");
|
|
ObjectNode arguments = params.putObject("arguments");
|
|
arguments.put("prompt", "전체 매출을 조회해 줘");
|
|
ObjectNode plan = arguments.putObject("queryPlan");
|
|
plan.put("targetType", "NONE");
|
|
plan.put("status", "NO_TARGET");
|
|
plan.putArray("targets").addObject()
|
|
.put("status", "NONE")
|
|
.putNull("gameKey")
|
|
.putNull("gamePrefix")
|
|
.putNull("userMasterObjectName");
|
|
|
|
ObjectNode response = service.handle("default", request, "user-bearer");
|
|
|
|
CapturingSelectAiService agentService = (CapturingSelectAiService) selectAiService;
|
|
assertThat(agentService.prompt).isEqualTo("전체 매출을 조회해 줘");
|
|
assertThat(agentService.queryPlan.path("targetType").asText()).isEqualTo("NONE");
|
|
assertThat(response.path("result").path("isError").asBoolean()).isFalse();
|
|
String payload = response.path("result").path("content").get(0).path("text").asText();
|
|
assertThat(payload)
|
|
.contains("oracle.select_ai.test_fewshot_nl2sql")
|
|
.contains("SHOWSQL_AND_EXECUTED")
|
|
.contains("generatedSql")
|
|
.contains("READ_ONLY_EXECUTED");
|
|
}
|
|
|
|
@Test
|
|
void returnsToolLevelDeniedResultWhenVpdTokenIsMissing() {
|
|
ObjectNode request = request(3, "tools/call");
|
|
ObjectNode params = (ObjectNode) request.putObject("params");
|
|
params.put("name", "oracle.select_ai.test_data_text2sql");
|
|
params.putObject("arguments").put("prompt", "카제나 AU를 조회해 줘");
|
|
|
|
ObjectNode response = service.handle("default", request, "");
|
|
|
|
assertThat(response.path("error").isMissingNode()).isTrue();
|
|
assertThat(response.path("result").path("isError").asBoolean()).isTrue();
|
|
assertThat(response.path("result").path("content").get(0).path("text").asText())
|
|
.contains("VPD_TOKEN_DENIED")
|
|
.contains("권한이 없습니다");
|
|
}
|
|
|
|
@Test
|
|
void searchesQaVectorExamplesForFewShotContext() {
|
|
ObjectNode request = request(5, "tools/call");
|
|
ObjectNode params = (ObjectNode) request.putObject("params");
|
|
params.put("name", "oracle.select_ai.test_qa_vector_search");
|
|
params.putObject("arguments").put("question", "active users by game").put("topK", 2);
|
|
|
|
ObjectNode response = service.handle("default", request, "user-bearer");
|
|
|
|
CapturingQaVectorService vectorService = (CapturingQaVectorService) qaVectorService;
|
|
assertThat(vectorService.bearerToken).isEqualTo("user-bearer");
|
|
assertThat(vectorService.question).isEqualTo("active users by game");
|
|
assertThat(vectorService.topK).isEqualTo(2);
|
|
assertThat(response.path("result").path("content").get(0).path("text").asText())
|
|
.contains("QA_VECTOR_SEARCH")
|
|
.contains("few-shot")
|
|
.contains("SELECT COUNT(*) FROM APP_USER");
|
|
}
|
|
|
|
@Test
|
|
void storesReviewedSelectAiExampleSql() {
|
|
ObjectNode request = request(6, "tools/call");
|
|
ObjectNode params = (ObjectNode) request.putObject("params");
|
|
params.put("name", "oracle.select_ai.test_qa_vector_store");
|
|
params.putObject("arguments")
|
|
.put("question", "active users by game")
|
|
.put("answerSql", "SELECT COUNT(*) FROM APP_USER")
|
|
.put("answer", "AU count");
|
|
|
|
ObjectNode response = service.handle("default", request, "user-bearer");
|
|
|
|
CapturingQaVectorService vectorService = (CapturingQaVectorService) qaVectorService;
|
|
assertThat(vectorService.answerSql).isEqualTo("SELECT COUNT(*) FROM APP_USER");
|
|
assertThat(response.path("result").path("content").get(0).path("text").asText())
|
|
.contains("QA_VECTOR_STORED")
|
|
.contains("exampleId");
|
|
}
|
|
|
|
@Test
|
|
void resolvesGameScopeFromTheDatabaseBackedTool() {
|
|
ObjectNode request = request(8, "tools/call");
|
|
ObjectNode params = (ObjectNode) request.putObject("params");
|
|
params.put("name", "oracle.select_ai.test_game_scope_resolve");
|
|
params.putObject("arguments").put("question", "games requested");
|
|
|
|
ObjectNode response = service.handle("default", request, "user-bearer");
|
|
|
|
assertThat(response.path("result").path("isError").asBoolean()).isFalse();
|
|
String payload = response.path("result").path("content").get(0).path("text").asText();
|
|
assertThat(payload)
|
|
.contains("RESOLVED")
|
|
.contains("CALL_FEW_SHOT_NL2SQL")
|
|
.contains("REPORT_UNSUPPORTED");
|
|
}
|
|
|
|
@Test
|
|
void keepsUnmatchedMentionsInPartialGameQueryPlan() {
|
|
ObjectNode request = request(9, "tools/call");
|
|
ObjectNode params = (ObjectNode) request.putObject("params");
|
|
params.put("name", "oracle.select_ai.game_query_plan");
|
|
params.putObject("arguments")
|
|
.put("question", "supported game and unknown game");
|
|
|
|
ObjectNode response = service.handle("default", request, "user-bearer");
|
|
|
|
assertThat(response.path("result").path("isError").asBoolean()).isFalse();
|
|
String payload = response.path("result").path("content").get(0)
|
|
.path("text").asText();
|
|
assertThat(payload)
|
|
.contains("\"status\" : \"PARTIAL\"")
|
|
.contains("\"gameKey\" : \"SUPPORTED_GAME\"")
|
|
.contains("\"mention\" : \"Unknown game\"")
|
|
.contains("\"reasonCode\" : \"LLM_NO_CANDIDATE_MATCH\"")
|
|
.contains("\"targetType\" : \"MULTI\"")
|
|
.contains("\"nextAction\" : \"CALL_FEWSHOT\"");
|
|
}
|
|
|
|
private ObjectNode request(int id, String method) {
|
|
ObjectNode request = objectMapper.createObjectNode();
|
|
request.put("jsonrpc", "2.0");
|
|
request.put("id", id);
|
|
request.put("method", method);
|
|
return request;
|
|
}
|
|
|
|
private static final class CapturingSelectAiService extends SelectAiService {
|
|
|
|
private String bearerToken;
|
|
private String prompt;
|
|
private JsonNode queryPlan;
|
|
private boolean showpromptCalled;
|
|
|
|
private CapturingSelectAiService() {
|
|
super(null, null, null, new ObjectMapper(), null, null, null);
|
|
}
|
|
|
|
@Override
|
|
public JsonNode generateAndExecute(String bearerToken, String prompt) {
|
|
this.bearerToken = bearerToken;
|
|
this.prompt = prompt;
|
|
ObjectNode response = new ObjectMapper().createObjectNode()
|
|
.put("profile", "SGMP_POC_OCI_GPT54MINI")
|
|
.put("generatedSql", "SELECT 1 FROM DUAL")
|
|
.put("status", "SHOWSQL_AND_EXECUTED")
|
|
.put("execution", "READ_ONLY_EXECUTED")
|
|
.put("rowCount", 1);
|
|
response.putArray("items").addObject().put("RESULT", 1);
|
|
return response;
|
|
}
|
|
|
|
@Override
|
|
public JsonNode generateAndExecute(String bearerToken, String prompt, String scopeGameKey) {
|
|
return generateAndExecute(bearerToken, prompt);
|
|
}
|
|
|
|
@Override
|
|
public JsonNode generateAndExecute(
|
|
String bearerToken, String prompt, String scopeGameKey, JsonNode priorToolContext) {
|
|
this.queryPlan = priorToolContext;
|
|
return generateAndExecute(bearerToken, prompt);
|
|
}
|
|
|
|
@Override
|
|
public JsonNode generatePrompt(String bearerToken, String prompt) {
|
|
this.bearerToken = bearerToken;
|
|
this.prompt = prompt;
|
|
this.showpromptCalled = true;
|
|
return new ObjectMapper().createObjectNode()
|
|
.put("profile", "SGMP_POC_OCI_GPT54MINI")
|
|
.put("status", "SHOWPROMPT")
|
|
.put("selectAiPrompt", "assembled Select AI prompt");
|
|
}
|
|
}
|
|
|
|
private static final class CapturingQaVectorService extends QaVectorService {
|
|
|
|
private String bearerToken;
|
|
private String question;
|
|
private String answerSql;
|
|
private int topK;
|
|
|
|
private CapturingQaVectorService() {
|
|
super(null, null, null);
|
|
}
|
|
|
|
@Override
|
|
public VectorSearchResult search(String bearerToken, String question, int topK) {
|
|
this.bearerToken = bearerToken;
|
|
this.question = question;
|
|
this.topK = topK;
|
|
return new VectorSearchResult(question, topK, java.util.List.of(new VectorExample(
|
|
42L, "active user count", "SELECT COUNT(*) FROM APP_USER", "AU count",
|
|
"cohere.embed-v4.0", "SQL_TEMPLATE", "ANY", null,
|
|
"STD-13", "CUSTOMER_QA_BENCHMARK", 0.12
|
|
)));
|
|
}
|
|
|
|
@Override
|
|
public VectorStoreResult store(String bearerToken, String question, String answerSql, String answer) {
|
|
this.bearerToken = bearerToken;
|
|
this.question = question;
|
|
this.answerSql = answerSql;
|
|
return new VectorStoreResult(77L, question, "cohere.embed-v4.0", "DRAFT");
|
|
}
|
|
}
|
|
|
|
private static final class CapturingGameScopeService extends GameScopeService {
|
|
|
|
private CapturingGameScopeService() {
|
|
super(null, new GameScopeProperties(false, "", 8), null, null);
|
|
}
|
|
|
|
@Override
|
|
public GameScopeResult resolve(String bearerToken, String question) {
|
|
return new GameScopeResult(question, "RESOLVED", java.util.List.of(
|
|
new GameScope("SUPPORTED_GAME", "Supported game", "Supported", "SUPPORTED",
|
|
"APPROVED_OBJECT_AVAILABLE", 100, "100"),
|
|
new GameScope("UNSUPPORTED_GAME", "Unsupported game", "Unsupported", "UNSUPPORTED",
|
|
"OBJECT_LIST_NOT_AVAILABLE", 100, "100")
|
|
));
|
|
}
|
|
}
|
|
|
|
private static final class CapturingGameCatalogVectorService
|
|
extends GameCatalogVectorService {
|
|
|
|
private CapturingGameCatalogVectorService() {
|
|
super(null, null, new ObjectMapper());
|
|
}
|
|
|
|
@Override
|
|
public ObjectNode queryPlan(String token, String question, int topK) {
|
|
ObjectNode plan = new ObjectMapper().createObjectNode();
|
|
plan.put("targetType", "MULTI");
|
|
plan.put("scopeType", "MULTI");
|
|
plan.put("status", "PARTIAL");
|
|
plan.putArray("targets")
|
|
.addObject()
|
|
.put("mention", "Supported game")
|
|
.put("status", "MATCHED")
|
|
.put("gameKey", "SUPPORTED_GAME")
|
|
.put("gamePrefix", "SUPPORTED")
|
|
.put("userMasterObjectName", "SUPPORTED_COMN_USER_MST");
|
|
plan.withArray("targets")
|
|
.addObject()
|
|
.put("mention", "Unknown game")
|
|
.put("status", "UNMATCHED")
|
|
.putNull("gameKey")
|
|
.putNull("gamePrefix")
|
|
.putNull("userMasterObjectName");
|
|
plan.putArray("matchedGames")
|
|
.addObject()
|
|
.put("gameKey", "SUPPORTED_GAME");
|
|
plan.putArray("mentionResults");
|
|
plan.putArray("supportedGames")
|
|
.addObject()
|
|
.put("gameKey", "SUPPORTED_GAME");
|
|
plan.putArray("unmatchedGames")
|
|
.addObject()
|
|
.put("mention", "Unknown game")
|
|
.put("reasonCode", "LLM_NO_CANDIDATE_MATCH");
|
|
plan.put("nextAction", "CALL_FEWSHOT");
|
|
plan.put("executionMode", "COMBINED");
|
|
return plan;
|
|
}
|
|
}
|
|
}
|