From bd651eb66fab71cdcf6834483bc2d221c2fb36a7 Mon Sep 17 00:00:00 2001 From: devmrko Date: Mon, 27 Jul 2026 13:58:37 +0900 Subject: [PATCH] inject game catalog context into few-shot prompt --- .../service/SelectAiService.java | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/SelectAiService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/SelectAiService.java index 0adf640..06aa64e 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/SelectAiService.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/SelectAiService.java @@ -49,6 +49,7 @@ public class SelectAiService { private final ObjectMapper objectMapper; private final QaVectorService qaVectorService; private final GameScopeService gameScopeService; + private final GameCatalogVectorService gameCatalogVectorService; public SelectAiService( BackofficeProperties properties, @@ -56,7 +57,8 @@ public class SelectAiService { Clock clock, ObjectMapper objectMapper, QaVectorService qaVectorService, - GameScopeService gameScopeService + GameScopeService gameScopeService, + GameCatalogVectorService gameCatalogVectorService ) { this.properties = properties; this.bearerTokenService = bearerTokenService; @@ -64,6 +66,7 @@ public class SelectAiService { this.objectMapper = objectMapper; this.qaVectorService = qaVectorService; this.gameScopeService = gameScopeService; + this.gameCatalogVectorService = gameCatalogVectorService; } public JsonNode generateAndExecute(String bearerToken, String prompt) { @@ -80,7 +83,9 @@ public class SelectAiService { BackofficeProperties.SelectAi selectAi = requiredSelectAi(); ResolvedExecutionScope scope = resolveExecutionScope(bearerToken, normalizedPrompt, scopeGameKey); - EnrichedPrompt enrichedPrompt = enrichWithFewShot(bearerToken, selectAi, scope.prompt()); + GameContext gameContext = resolveGameContext(bearerToken, normalizedPrompt); + EnrichedPrompt enrichedPrompt = enrichWithFewShot( + bearerToken, selectAi, gameContext.prompt(scope.prompt())); String generatedSql = generate(selectAi, enrichedPrompt.prompt(), "showsql"); String normalizedSql = validateReadOnlySql(generatedSql); QueryExecution execution = executeReadOnly(selectAi, normalizedSql); @@ -88,6 +93,9 @@ public class SelectAiService { response.put("status", "SHOWSQL_AND_EXECUTED"); response.put("profile", selectAi.profile()); response.put("originalPrompt", normalizedPrompt); + response.put("gameScopeType", gameContext.scopeType()); + response.put("gameScopeStatus", gameContext.status()); + response.put("gameCandidateCount", gameContext.candidates().size()); if (scope.gameKey() != null) { response.put("scopeGameKey", scope.gameKey()); response.put("scopeDisplayName", scope.displayName()); @@ -107,6 +115,18 @@ public class SelectAiService { return response; } + private GameContext resolveGameContext(String bearerToken, String question) { + try { + List candidates = + gameCatalogVectorService.search(bearerToken, question, 5); + String status = candidates.isEmpty() ? "NO_MATCH" : "RESOLVED"; + String scopeType = candidates.isEmpty() ? "UNKNOWN" : "SINGLE_GAME"; + return new GameContext(scopeType, status, candidates); + } catch (Exception ignored) { + return new GameContext("UNKNOWN", "UNAVAILABLE", List.of()); + } + } + private ResolvedExecutionScope resolveExecutionScope( String bearerToken, String originalPrompt, String scopeGameKey ) { @@ -397,4 +417,27 @@ public class SelectAiService { String prompt, String status, int exampleCount, List examples) {} private record ResolvedExecutionScope(String prompt, String gameKey, String displayName) {} + + private record GameContext( + String scopeType, String status, List candidates) { + String prompt(String original) { + StringBuilder context = new StringBuilder(); + context.append("[GAME CATALOG MATCH]\n") + .append("scope_type: ").append(scopeType).append('\n') + .append("status: ").append(status).append('\n'); + for (GameCatalogVectorService.GameCandidate candidate : candidates) { + context.append("game_key: ").append(candidate.gameKey()).append('\n') + .append("game_id: ").append(candidate.gameId()).append('\n') + .append("game_prefix: ").append(candidate.gamePrefix()).append('\n') + .append("game_name: ").append(candidate.gameName()).append('\n') + .append("matched_aliases: ").append(candidate.aliases()).append('\n') + .append("cosine_distance: ").append(candidate.similarity()).append('\n'); + } + context.append("[GAME SCOPE POLICY]\n") + .append("Use only DB-approved game scope when status is RESOLVED. ") + .append("When status is NO_MATCH or UNKNOWN, do not infer a game-specific object.\n\n") + .append(original); + return context.toString(); + } + } }