refs #739: govern Smilegate few-shot references
This commit is contained in:
@@ -255,6 +255,11 @@ public class McpSseService {
|
||||
item.put("answer", example.answer());
|
||||
}
|
||||
item.put("embeddingModel", example.embeddingModel());
|
||||
item.put("referenceKind", example.referenceKind());
|
||||
item.put("targetType", example.targetType());
|
||||
if (example.objectRole() != null) {
|
||||
item.put("objectRole", example.objectRole());
|
||||
}
|
||||
item.put("cosineDistance", example.cosineDistance());
|
||||
}
|
||||
return response;
|
||||
@@ -267,6 +272,8 @@ public class McpSseService {
|
||||
response.put("exampleId", stored.exampleId());
|
||||
response.put("question", stored.question());
|
||||
response.put("embeddingModel", stored.embeddingModel());
|
||||
response.put("referenceStatus", stored.referenceStatus());
|
||||
response.put("nextStep", "DRAFT 예제입니다. 실행·정책 검토 후 APPROVED로 전환해야 Few-shot 검색에 사용됩니다.");
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,20 +44,28 @@ public class QaVectorService {
|
||||
}
|
||||
|
||||
public VectorSearchResult search(String bearerToken, String question, int topK) {
|
||||
return search(bearerToken, question, topK, "ANY");
|
||||
}
|
||||
|
||||
/** Searches only approved examples compatible with the current game target contract. */
|
||||
public VectorSearchResult search(
|
||||
String bearerToken, String question, int topK, String targetType) {
|
||||
requireActiveToken(bearerToken);
|
||||
String normalizedQuestion = requiredText(question, "question", MAX_QUESTION_LENGTH);
|
||||
if (topK < 1 || topK > 20) {
|
||||
throw new AppException("topK는 1에서 20 사이여야 합니다.");
|
||||
}
|
||||
String normalizedTargetType = requiredTargetType(targetType);
|
||||
BackofficeProperties.SelectAi selectAi = requiredSelectAi();
|
||||
|
||||
List<VectorExample> examples = new ArrayList<>();
|
||||
try (Connection connection = DriverManager.getConnection(
|
||||
selectAi.dbUrl(), selectAi.dbUsername(), selectAi.dbPassword());
|
||||
CallableStatement statement = connection.prepareCall("{ ? = call sg_qa_vector_search(?, ?) }")) {
|
||||
CallableStatement statement = connection.prepareCall("{ ? = call sg_qa_vector_search(?, ?, ?) }")) {
|
||||
statement.registerOutParameter(1, Types.REF_CURSOR);
|
||||
statement.setString(2, normalizedQuestion);
|
||||
statement.setInt(3, topK);
|
||||
statement.setString(4, normalizedTargetType);
|
||||
statement.execute();
|
||||
try (ResultSet resultSet = (ResultSet) statement.getObject(1)) {
|
||||
while (resultSet.next()) {
|
||||
@@ -67,6 +75,9 @@ public class QaVectorService {
|
||||
resultSet.getString("ANSWER_SQL"),
|
||||
resultSet.getString("ANSWER_TEXT"),
|
||||
resultSet.getString("EMBEDDING_MODEL"),
|
||||
resultSet.getString("REFERENCE_KIND"),
|
||||
resultSet.getString("TARGET_TYPE"),
|
||||
resultSet.getString("OBJECT_ROLE"),
|
||||
resultSet.getDouble("COSINE_DISTANCE")
|
||||
));
|
||||
}
|
||||
@@ -96,7 +107,7 @@ public class QaVectorService {
|
||||
throw new AppException("QA 벡터 예제 SQL 저장 결과가 없습니다.");
|
||||
}
|
||||
return new VectorStoreResult(
|
||||
resultSet.getLong("EXAMPLE_ID"), normalizedQuestion, "cohere.embed-v4.0");
|
||||
resultSet.getLong("EXAMPLE_ID"), normalizedQuestion, "cohere.embed-v4.0", "DRAFT");
|
||||
}
|
||||
} catch (AppException exception) {
|
||||
throw exception;
|
||||
@@ -145,6 +156,14 @@ public class QaVectorService {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String requiredTargetType(String value) {
|
||||
String normalized = value == null ? "ANY" : value.trim().toUpperCase();
|
||||
if (!List.of("NONE", "SINGLE", "MULTI", "ALL", "ANY").contains(normalized)) {
|
||||
throw new AppException("targetType은 NONE, SINGLE, MULTI, ALL, ANY 중 하나여야 합니다.");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String optionalText(String value, String fieldName, int maximumLength) {
|
||||
String normalized = value == null ? "" : value.trim();
|
||||
if (normalized.isEmpty()) {
|
||||
@@ -162,6 +181,9 @@ public class QaVectorService {
|
||||
String answerSql,
|
||||
String answer,
|
||||
String embeddingModel,
|
||||
String referenceKind,
|
||||
String targetType,
|
||||
String objectRole,
|
||||
double cosineDistance
|
||||
) {
|
||||
}
|
||||
@@ -169,6 +191,7 @@ public class QaVectorService {
|
||||
public record VectorSearchResult(String question, int topK, List<VectorExample> examples) {
|
||||
}
|
||||
|
||||
public record VectorStoreResult(long exampleId, String question, String embeddingModel) {
|
||||
public record VectorStoreResult(
|
||||
long exampleId, String question, String embeddingModel, String referenceStatus) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ public class SelectAiService {
|
||||
QueryPlanContext queryPlan
|
||||
) {
|
||||
EnrichedPrompt enrichedPrompt = enrichWithFewShot(
|
||||
bearerToken, selectAi, executionPrompt);
|
||||
bearerToken, selectAi, executionPrompt, queryPlan == null ? "ANY" : queryPlan.targetType());
|
||||
String generatedSql = generate(selectAi, enrichedPrompt.prompt(), "showsql");
|
||||
String normalizedSql = validateReadOnlySql(generatedSql);
|
||||
if (allowedPrefixes != null && gameScopeService != null
|
||||
@@ -287,6 +287,11 @@ public class SelectAiService {
|
||||
item.put("answer", example.answer());
|
||||
}
|
||||
item.put("embeddingModel", example.embeddingModel());
|
||||
item.put("referenceKind", example.referenceKind());
|
||||
item.put("targetType", example.targetType());
|
||||
if (example.objectRole() != null) {
|
||||
item.put("objectRole", example.objectRole());
|
||||
}
|
||||
item.put("cosineDistance", example.cosineDistance());
|
||||
}
|
||||
}
|
||||
@@ -296,7 +301,7 @@ public class SelectAiService {
|
||||
requireActiveToken(bearerToken);
|
||||
String normalizedPrompt = requiredPrompt(prompt);
|
||||
BackofficeProperties.SelectAi selectAi = requiredSelectAi();
|
||||
EnrichedPrompt enrichedPrompt = enrichWithFewShot(bearerToken, selectAi, normalizedPrompt);
|
||||
EnrichedPrompt enrichedPrompt = enrichWithFewShot(bearerToken, selectAi, normalizedPrompt, "ANY");
|
||||
String selectAiPrompt = generate(selectAi, enrichedPrompt.prompt(), "showprompt");
|
||||
|
||||
ObjectNode response = objectMapper.createObjectNode();
|
||||
@@ -370,14 +375,15 @@ public class SelectAiService {
|
||||
private EnrichedPrompt enrichWithFewShot(
|
||||
String bearerToken,
|
||||
BackofficeProperties.SelectAi selectAi,
|
||||
String prompt
|
||||
String prompt,
|
||||
String targetType
|
||||
) {
|
||||
if (!fewShotEnabled(selectAi) || qaVectorService == null) {
|
||||
return new EnrichedPrompt(prompt, "DISABLED", 0, List.of());
|
||||
}
|
||||
try {
|
||||
List<QaVectorService.VectorExample> examples = qaVectorService
|
||||
.search(bearerToken, prompt, fewShotTopK(selectAi))
|
||||
.search(bearerToken, prompt, fewShotTopK(selectAi), targetType)
|
||||
.examples();
|
||||
if (examples.isEmpty()) {
|
||||
return new EnrichedPrompt(composePolicyPrompt(prompt), "NO_MATCH", 0, List.of());
|
||||
@@ -412,12 +418,10 @@ public class SelectAiService {
|
||||
if (included >= MAX_FEW_SHOT_EXAMPLES) {
|
||||
break;
|
||||
}
|
||||
String answerSql = truncate(example.answerSql(), MAX_FEW_SHOT_SQL_CHARS);
|
||||
if (answerSql.isBlank()) {
|
||||
String candidate = referenceCandidate(included + 1, example);
|
||||
if (candidate.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
String candidate = "Example " + (included + 1) + " question:\n" + example.question()
|
||||
+ "\nExample " + (included + 1) + " verified SQL:\n" + answerSql + "\n\n";
|
||||
if (enriched.length() + candidate.length() + prompt.length() > MAX_ENRICHED_PROMPT_LENGTH) {
|
||||
break;
|
||||
}
|
||||
@@ -430,6 +434,19 @@ public class SelectAiService {
|
||||
return enriched.append("Original user question:\n").append(prompt).toString();
|
||||
}
|
||||
|
||||
private static String referenceCandidate(int index, QaVectorService.VectorExample example) {
|
||||
String prefix = "Example " + index + " question:\n" + example.question()
|
||||
+ "\nExample " + index + " target type: " + example.targetType()
|
||||
+ "\nExample " + index + " reference kind: " + example.referenceKind() + "\n";
|
||||
if ("NO_TARGET".equals(example.referenceKind())
|
||||
|| "OBJECT_UNAVAILABLE".equals(example.referenceKind())) {
|
||||
String boundary = truncate(example.answer(), MAX_FEW_SHOT_SQL_CHARS);
|
||||
return boundary.isBlank() ? "" : prefix + "Boundary outcome:\n" + boundary + "\n\n";
|
||||
}
|
||||
String answerSql = truncate(example.answerSql(), MAX_FEW_SHOT_SQL_CHARS);
|
||||
return answerSql.isBlank() ? "" : prefix + "Verified SQL template:\n" + answerSql + "\n\n";
|
||||
}
|
||||
|
||||
private static String truncate(String value, int maxLength) {
|
||||
String normalized = value == null ? "" : value.trim();
|
||||
return normalized.length() <= maxLength ? normalized : normalized.substring(0, maxLength);
|
||||
|
||||
Reference in New Issue
Block a user