refs #731: add QA vector MCP tools

This commit is contained in:
devmrko
2026-07-24 14:41:00 +09:00
parent 744dcad634
commit df19912378
8 changed files with 522 additions and 25 deletions

View File

@@ -20,17 +20,20 @@ public class McpSseService {
private final ObjectMapper objectMapper;
private final BackofficeProperties properties;
private final McpProperties mcpProperties;
private final QaVectorService qaVectorService;
public McpSseService(
SelectAiService selectAiService,
ObjectMapper objectMapper,
BackofficeProperties properties,
McpProperties mcpProperties
McpProperties mcpProperties,
QaVectorService qaVectorService
) {
this.selectAiService = selectAiService;
this.objectMapper = objectMapper;
this.properties = properties;
this.mcpProperties = mcpProperties;
this.qaVectorService = qaVectorService;
}
public ObjectNode handle(String contextPath, JsonNode request) {
@@ -69,7 +72,8 @@ public class McpSseService {
/** Tools registered by this MCP server. */
public List<McpToolView> registeredTools() {
return List.of(selectAiQueryView(), selectAiShowpromptView());
return List.of(
selectAiQueryView(), selectAiShowpromptView(), qaVectorSearchView(), qaVectorStoreView());
}
private ObjectNode initializeResult(String contextPath) {
@@ -90,6 +94,8 @@ public class McpSseService {
ArrayNode tools = objectMapper.createArrayNode();
tools.add(toolDefinition(selectAiQueryView()));
tools.add(toolDefinition(selectAiShowpromptView()));
tools.add(toolDefinition(qaVectorSearchView()));
tools.add(toolDefinition(qaVectorStoreView()));
result.set("tools", tools);
return result;
}
@@ -103,26 +109,47 @@ public class McpSseService {
schema.put("type", "object");
ObjectNode properties = objectMapper.createObjectNode();
ObjectNode prompt = objectMapper.createObjectNode();
prompt.put("type", "string");
prompt.put("description", promptDescription());
prompt.put("maxLength", 4000);
properties.set("prompt", prompt);
schema.set("properties", properties);
ArrayNode required = objectMapper.createArrayNode();
required.add("prompt");
if (qaVectorSearchToolName().equals(toolView.name())) {
addStringProperty(properties, "question", "few-shot 예제 SQL을 찾을 현재 질문입니다.", 4000);
ObjectNode topK = properties.putObject("topK");
topK.put("type", "integer");
topK.put("description", "반환할 유사 예제 수입니다. 기본값은 3입니다.");
topK.put("minimum", 1);
topK.put("maximum", 20);
topK.put("default", 3);
required.add("question");
} else if (qaVectorStoreToolName().equals(toolView.name())) {
addStringProperty(properties, "question", "검토된 Select AI 예제가 답한 업무 질문입니다.", 4000);
addStringProperty(properties, "answerSql", "검토된 단일 읽기 전용 SELECT/WITH SQL입니다.", 20000);
addStringProperty(properties, "answer", "선택 사항인 답변 또는 검토 메모입니다.", 20000);
required.add("question");
required.add("answerSql");
} else {
addStringProperty(properties, "prompt", promptDescription(), 4000);
required.add("prompt");
}
schema.set("properties", properties);
schema.set("required", required);
schema.put("additionalProperties", false);
item.set("inputSchema", schema);
return item;
}
private void addStringProperty(ObjectNode properties, String name, String description, int maxLength) {
ObjectNode property = properties.putObject(name);
property.put("type", "string");
property.put("description", description);
property.put("maxLength", maxLength);
}
private ObjectNode toolsCallResult(JsonNode params, String vpdBearerToken) {
String calledToolName = params.path("name").asText("");
boolean queryTool = toolName().equals(calledToolName);
boolean showpromptTool = showpromptToolName().equals(calledToolName);
if (!queryTool && !showpromptTool) {
boolean qaVectorSearchTool = qaVectorSearchToolName().equals(calledToolName);
boolean qaVectorStoreTool = qaVectorStoreToolName().equals(calledToolName);
if (!queryTool && !showpromptTool && !qaVectorSearchTool && !qaVectorStoreTool) {
throw new AppException("등록되지 않은 MCP tool입니다: " + calledToolName);
}
@@ -133,10 +160,22 @@ public class McpSseService {
}
JsonNode response;
try {
String prompt = arguments.path("prompt").asText("");
response = queryTool
? selectAiService.generateAndExecute(token, prompt)
: selectAiService.generatePrompt(token, prompt);
if (qaVectorSearchTool) {
response = qaVectorSearchResponse(
qaVectorService.search(token, arguments.path("question").asText(""), arguments.path("topK").asInt(3)));
} else if (qaVectorStoreTool) {
response = qaVectorStoreResponse(qaVectorService.store(
token,
arguments.path("question").asText(""),
arguments.path("answerSql").asText(""),
arguments.path("answer").isMissingNode() ? null : arguments.path("answer").asText(null)
));
} else {
String prompt = arguments.path("prompt").asText("");
response = queryTool
? selectAiService.generateAndExecute(token, prompt)
: selectAiService.generatePrompt(token, prompt);
}
} catch (VpdTokenAccessDeniedException ignored) {
return tokenAccessDeniedResult();
}
@@ -158,6 +197,37 @@ public class McpSseService {
return result;
}
private ObjectNode qaVectorSearchResponse(QaVectorService.VectorSearchResult result) {
ObjectNode response = objectMapper.createObjectNode();
response.put("status", "QA_VECTOR_SEARCH");
response.put("instruction", "Select AI SQL 생성 전에 few-shot 예제 SQL 후보를 확인합니다.");
response.put("question", result.question());
response.put("topK", result.topK());
ArrayNode examples = response.putArray("examples");
for (QaVectorService.VectorExample example : result.examples()) {
ObjectNode item = examples.addObject();
item.put("exampleId", example.exampleId());
item.put("question", example.question());
item.put("answerSql", example.answerSql());
if (example.answer() != null) {
item.put("answer", example.answer());
}
item.put("embeddingModel", example.embeddingModel());
item.put("cosineDistance", example.cosineDistance());
}
return response;
}
private ObjectNode qaVectorStoreResponse(QaVectorService.VectorStoreResult stored) {
ObjectNode response = objectMapper.createObjectNode();
response.put("status", "QA_VECTOR_STORED");
response.put("instruction", "검토된 Select AI 결과를 후속 Text2SQL 품질 향상용 예제 SQL로 저장했습니다.");
response.put("exampleId", stored.exampleId());
response.put("question", stored.question());
response.put("embeddingModel", stored.embeddingModel());
return response;
}
private ObjectNode tokenAccessDeniedResult() {
ObjectNode payload = objectMapper.createObjectNode();
payload.put("status", "VPD_TOKEN_DENIED");
@@ -196,6 +266,18 @@ public class McpSseService {
);
}
private McpToolView qaVectorSearchView() {
return new McpToolView(
qaVectorSearchToolName(), qaVectorSearchToolDescription(), -1L,
qaVectorSearchToolLabel(), SELECT_AI_TOOL_PATH);
}
private McpToolView qaVectorStoreView() {
return new McpToolView(
qaVectorStoreToolName(), qaVectorStoreToolDescription(), -1L,
qaVectorStoreToolLabel(), SELECT_AI_TOOL_PATH);
}
private String selectAiProfile() {
BackofficeProperties.SelectAi selectAi = properties == null ? null : properties.selectAi();
if (selectAi == null || selectAi.profile() == null || selectAi.profile().isBlank()) {
@@ -240,6 +322,38 @@ public class McpSseService {
: mcpProperties.resolvedShowpromptToolDescription();
}
private String qaVectorSearchToolName() {
return mcpProperties == null ? "oracle.select_ai.qa_vector_search"
: mcpProperties.resolvedQaVectorSearchToolName();
}
private String qaVectorSearchToolLabel() {
return mcpProperties == null ? "Select AI 예제 SQL 조회"
: mcpProperties.resolvedQaVectorSearchToolLabel();
}
private String qaVectorSearchToolDescription() {
return mcpProperties == null
? "현재 질문에 사용할 유사 예제 SQL을 Select AI 실행 전에 조회합니다."
: mcpProperties.resolvedQaVectorSearchToolDescription();
}
private String qaVectorStoreToolName() {
return mcpProperties == null ? "oracle.select_ai.qa_vector_store"
: mcpProperties.resolvedQaVectorStoreToolName();
}
private String qaVectorStoreToolLabel() {
return mcpProperties == null ? "Select AI 예제 SQL 저장"
: mcpProperties.resolvedQaVectorStoreToolLabel();
}
private String qaVectorStoreToolDescription() {
return mcpProperties == null
? "검토된 Select AI 결과를 후속 Text2SQL 품질 향상용 예제 SQL로 저장합니다."
: mcpProperties.resolvedQaVectorStoreToolDescription();
}
private String pretty(Object value) {
try {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);