generalize game scope guidance through metadata few-shot

This commit is contained in:
devmrko
2026-07-27 13:03:33 +09:00
parent 0cbac8d23b
commit 0662c4140c

View File

@@ -48,27 +48,39 @@ public class SelectAiService {
private final Clock clock;
private final ObjectMapper objectMapper;
private final QaVectorService qaVectorService;
private final GameScopeService gameScopeService;
public SelectAiService(
BackofficeProperties properties,
BearerTokenService bearerTokenService,
Clock clock,
ObjectMapper objectMapper,
QaVectorService qaVectorService
QaVectorService qaVectorService,
GameScopeService gameScopeService
) {
this.properties = properties;
this.bearerTokenService = bearerTokenService;
this.clock = clock;
this.objectMapper = objectMapper;
this.qaVectorService = qaVectorService;
this.gameScopeService = gameScopeService;
}
public JsonNode generateAndExecute(String bearerToken, String prompt) {
return generateAndExecute(bearerToken, prompt, null);
}
/**
* Executes one DB-resolved game scope. The optional key is revalidated for
* the original question; it is never a caller-provided table or prefix.
*/
public JsonNode generateAndExecute(String bearerToken, String prompt, String scopeGameKey) {
requireActiveToken(bearerToken);
String normalizedPrompt = requiredPrompt(prompt);
BackofficeProperties.SelectAi selectAi = requiredSelectAi();
ResolvedExecutionScope scope = resolveExecutionScope(bearerToken, normalizedPrompt, scopeGameKey);
EnrichedPrompt enrichedPrompt = enrichWithFewShot(bearerToken, selectAi, normalizedPrompt);
EnrichedPrompt enrichedPrompt = enrichWithFewShot(bearerToken, selectAi, scope.prompt());
String generatedSql = generate(selectAi, enrichedPrompt.prompt(), "showsql");
String normalizedSql = validateReadOnlySql(generatedSql);
QueryExecution execution = executeReadOnly(selectAi, normalizedSql);
@@ -76,6 +88,11 @@ public class SelectAiService {
response.put("status", "SHOWSQL_AND_EXECUTED");
response.put("profile", selectAi.profile());
response.put("originalPrompt", normalizedPrompt);
if (scope.gameKey() != null) {
response.put("scopeGameKey", scope.gameKey());
response.put("scopeDisplayName", scope.displayName());
response.put("scopeStatus", "DB_REVALIDATED");
}
response.put("fewShotStatus", enrichedPrompt.status());
response.put("fewShotExampleCount", enrichedPrompt.exampleCount());
addFewShotExamples(response, enrichedPrompt.examples());
@@ -90,6 +107,33 @@ public class SelectAiService {
return response;
}
private ResolvedExecutionScope resolveExecutionScope(
String bearerToken, String originalPrompt, String scopeGameKey
) {
String requestedKey = scopeGameKey == null ? "" : scopeGameKey.trim();
if (requestedKey.isEmpty()) {
return new ResolvedExecutionScope(originalPrompt, null, null);
}
if (gameScopeService == null) {
throw new AppException("게임 범위 검증 서비스를 사용할 수 없습니다.");
}
GameScopeService.GameScope scope = gameScopeService.resolve(bearerToken, originalPrompt).scopes().stream()
.filter(item -> requestedKey.equals(item.gameKey()))
.findFirst()
.orElseThrow(() -> new AppException("요청한 게임 범위가 DB 조회 결과에 없습니다."));
if (!"SUPPORTED".equals(scope.status())) {
throw new AppException("DB 게임 범위가 조회 실행을 허용하지 않습니다: " + scope.reasonCode());
}
String scopedPrompt = "Use only the DB-resolved game scope below. Do not generate SQL for any other "
+ "game mentioned in the original question. The scope was validated through current game alias "
+ "metadata and approved object availability.\n"
+ "Resolved game key: " + scope.gameKey() + "\n"
+ "Resolved display name: " + scope.displayName() + "\n"
+ "Matched alias: " + scope.matchedAlias() + "\n\n"
+ "Original user question:\n" + originalPrompt;
return new ResolvedExecutionScope(scopedPrompt, scope.gameKey(), scope.displayName());
}
private void addFewShotExamples(ObjectNode response, List<QaVectorService.VectorExample> examples) {
ArrayNode items = response.putArray("fewShotExamples");
for (QaVectorService.VectorExample example : examples) {
@@ -206,13 +250,19 @@ public class SelectAiService {
}
static String composePolicyPrompt(String prompt) {
return POLICY_PREFIX + "Original user question:\n" + prompt;
return POLICY_PREFIX
+ "Resolve business terms and game names from the approved game-alias metadata before selecting a "
+ "game-scoped object. A generic term such as common user means no particular game. If no game alias "
+ "is resolved, do not substitute an arbitrary game-scoped object; ask for the game or return no data.\n"
+ "Original user question:\n" + prompt;
}
static String composeFewShotPrompt(String prompt, List<QaVectorService.VectorExample> examples) {
StringBuilder enriched = new StringBuilder(POLICY_PREFIX
+ "The verified examples below are guidance only: use only relevant SQL patterns, do not invent "
+ "identifiers, and do not override current metadata or game-alias resolution policy.\n\n"
+ "identifiers, and do not override current metadata or game-alias resolution policy. "
+ "When examples use a game-specific object, reuse that pattern only after the current question "
+ "resolves the same game alias; otherwise keep the query unscoped or request clarification.\n\n"
+ "Verified few-shot examples:\n");
int included = 0;
for (QaVectorService.VectorExample example : examples) {
@@ -344,4 +394,6 @@ public class SelectAiService {
private record EnrichedPrompt(
String prompt, String status, int exampleCount, List<QaVectorService.VectorExample> examples) {}
private record ResolvedExecutionScope(String prompt, String gameKey, String displayName) {}
}