implement game catalog vector MCP resolver
This commit is contained in:
@@ -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;
|
||||||
|
/
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user