implement game catalog vector MCP resolver

This commit is contained in:
devmrko
2026-07-27 13:54:49 +09:00
parent 97cd5c9f25
commit 2e26e3261f
3 changed files with 78 additions and 3 deletions

View File

@@ -56,3 +56,25 @@ COMMENT ON TABLE sg_game_catalog IS
COMMENT ON COLUMN sg_game_catalog.embedding IS COMMENT ON COLUMN sg_game_catalog.embedding IS
'Vector representation of game names and aliases; populated by the configured embedding job.'; 'Vector representation of game names and aliases; populated by the configured embedding job.';
/ /
CREATE OR REPLACE FUNCTION sg_game_catalog_search(
p_question IN CLOB,
p_top_k IN PLS_INTEGER DEFAULT 5
) RETURN SYS_REFCURSOR AUTHID DEFINER IS
v_query VECTOR;
v_result SYS_REFCURSOR;
BEGIN
v_query := DBMS_VECTOR.UTL_TO_EMBEDDING(
p_question,
JSON(sg_qa_vector_params('search_query'))
);
OPEN v_result FOR
SELECT game_key, game_id, game_prefix, game_nm, game_alias_nm,
VECTOR_DISTANCE(embedding, v_query, COSINE) AS cosine_distance
FROM sg_game_catalog
WHERE active_yn = 'Y' AND embedding IS NOT NULL
ORDER BY VECTOR_DISTANCE(embedding, v_query, COSINE), priority, game_key
FETCH FIRST LEAST(GREATEST(NVL(p_top_k, 5), 1), 20) ROWS ONLY;
RETURN v_result;
END;
/

View File

@@ -0,0 +1,37 @@
package com.cloudhandson.vpdbackoffice.service;
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class GameCatalogVectorService {
private final BackofficeProperties properties;
private final BearerTokenService bearerTokenService;
public GameCatalogVectorService(BackofficeProperties properties, BearerTokenService bearerTokenService) {
this.properties = properties;
this.bearerTokenService = bearerTokenService;
}
public List<GameCandidate> search(String token, String question, int topK) {
if (token == null || token.isBlank() || bearerTokenService.findByPlainToken(token.trim()) == null) {
throw new VpdTokenAccessDeniedException();
}
BackofficeProperties.SelectAi db = properties.selectAi();
if (db == null || !db.configured()) throw new AppException("게임 카탈로그 DB 설정이 필요합니다.");
List<GameCandidate> result = new ArrayList<>();
try (Connection c = DriverManager.getConnection(db.dbUrl(), db.dbUsername(), db.dbPassword());
CallableStatement s = c.prepareCall("{ ? = call sg_game_catalog_search(?, ?) }")) {
s.registerOutParameter(1, Types.REF_CURSOR); s.setString(2, question); s.setInt(3, Math.min(Math.max(topK, 1), 20)); s.execute();
try (ResultSet rs = (ResultSet) s.getObject(1)) {
while (rs.next()) result.add(new GameCandidate(rs.getString("GAME_KEY"), rs.getString("GAME_ID"), rs.getString("GAME_PREFIX"), rs.getString("GAME_NM"), rs.getString("GAME_ALIAS_NM"), rs.getDouble("COSINE_DISTANCE")));
}
return result;
} catch (SQLException e) { throw new AppException("게임 카탈로그 벡터 검색 실패: " + e.getMessage()); }
}
public record GameCandidate(String gameKey, String gameId, String gamePrefix, String gameName, String aliases, double similarity) {}
}

View File

@@ -23,6 +23,7 @@ public class McpSseService {
private final McpProperties mcpProperties; private final McpProperties mcpProperties;
private final QaVectorService qaVectorService; private final QaVectorService qaVectorService;
private final GameScopeService gameScopeService; private final GameScopeService gameScopeService;
private final GameCatalogVectorService gameCatalogVectorService;
public McpSseService( public McpSseService(
SelectAiService selectAiService, SelectAiService selectAiService,
@@ -30,7 +31,8 @@ public class McpSseService {
BackofficeProperties properties, BackofficeProperties properties,
McpProperties mcpProperties, McpProperties mcpProperties,
QaVectorService qaVectorService, QaVectorService qaVectorService,
GameScopeService gameScopeService GameScopeService gameScopeService,
GameCatalogVectorService gameCatalogVectorService
) { ) {
this.selectAiService = selectAiService; this.selectAiService = selectAiService;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
@@ -38,6 +40,7 @@ public class McpSseService {
this.mcpProperties = mcpProperties; this.mcpProperties = mcpProperties;
this.qaVectorService = qaVectorService; this.qaVectorService = qaVectorService;
this.gameScopeService = gameScopeService; this.gameScopeService = gameScopeService;
this.gameCatalogVectorService = gameCatalogVectorService;
} }
public ObjectNode handle(String contextPath, JsonNode request) { public ObjectNode handle(String contextPath, JsonNode request) {
@@ -191,8 +194,8 @@ public class McpSseService {
response = gameScopeResponse(gameScopeService.resolve( response = gameScopeResponse(gameScopeService.resolve(
token, arguments.path("question").asText(""))); token, arguments.path("question").asText("")));
} else if (gameCatalogTool) { } else if (gameCatalogTool) {
response = gameScopeResponse(gameScopeService.resolve( response = gameCatalogVectorResponse(gameCatalogVectorService.search(
token, arguments.path("question").asText(""))); token, arguments.path("question").asText(""), arguments.path("topK").asInt(5)));
} else { } else {
String prompt = arguments.path("prompt").asText(""); String prompt = arguments.path("prompt").asText("");
response = queryTool response = queryTool
@@ -244,6 +247,19 @@ public class McpSseService {
return response; return response;
} }
private ObjectNode gameCatalogVectorResponse(List<GameCatalogVectorService.GameCandidate> candidates) {
ObjectNode response = objectMapper.createObjectNode();
response.put("status", candidates.isEmpty() ? "NO_MATCH" : "CANDIDATES");
ArrayNode items = response.putArray("matchedGames");
for (GameCatalogVectorService.GameCandidate c : candidates) {
ObjectNode item = items.addObject();
item.put("gameKey", c.gameKey()); item.put("gameId", c.gameId());
item.put("gamePrefix", c.gamePrefix()); item.put("gameName", c.gameName());
item.put("aliases", c.aliases()); item.put("cosineDistance", c.similarity());
}
return response;
}
private ObjectNode qaVectorStoreResponse(QaVectorService.VectorStoreResult stored) { private ObjectNode qaVectorStoreResponse(QaVectorService.VectorStoreResult stored) {
ObjectNode response = objectMapper.createObjectNode(); ObjectNode response = objectMapper.createObjectNode();
response.put("status", "QA_VECTOR_STORED"); response.put("status", "QA_VECTOR_STORED");