[Developer] #424 add LLM explanation for VPD policies

This commit is contained in:
devmrko
2026-06-25 13:03:37 +09:00
parent 1368699891
commit 9f9d8a66c5
5 changed files with 160 additions and 4 deletions

View File

@@ -0,0 +1,10 @@
package com.cloudhandson.vpdbackoffice.domain.vpd;
public record VpdPolicyExplanation(
String status,
String answer,
String prompt,
VpdPolicyDetail detail,
VpdFunctionSource functionSource
) {
}

View File

@@ -2,6 +2,7 @@ package com.cloudhandson.vpdbackoffice.service;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionSource;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyDetail;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyExplanation;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView;
import com.cloudhandson.vpdbackoffice.mapper.VpdPolicyMapper;
import java.util.List;
@@ -12,9 +13,11 @@ import org.springframework.stereotype.Service;
public class VpdPolicyService {
private final VpdPolicyMapper mapper;
private final OpenAiCompatibleClient aiClient;
public VpdPolicyService(VpdPolicyMapper mapper) {
public VpdPolicyService(VpdPolicyMapper mapper, OpenAiCompatibleClient aiClient) {
this.mapper = mapper;
this.aiClient = aiClient;
}
public List<VpdPolicyView> findPolicies() {
@@ -42,6 +45,90 @@ public class VpdPolicyService {
return new VpdPolicyDetail(policy, buildAddPolicyBlock(policy));
}
public VpdPolicyExplanation explainPolicy(String objectOwner, String objectName, String policyName) {
VpdPolicyDetail detail = findPolicyDetail(objectOwner, objectName, policyName);
VpdPolicyView policy = detail.policy();
VpdFunctionSource functionSource = findFunctionSource(
policy.functionOwner(),
policy.packageName(),
policy.functionName()
);
String prompt = buildExplanationPrompt(detail, functionSource);
if (!aiClient.configured()) {
return new VpdPolicyExplanation(
"AI_NOT_CONFIGURED",
"AI base URL/API Key 설정이 없어 모델 호출은 건너뛰었습니다. 아래 Prompt와 function source를 기준으로 policy/filter 내용을 확인하세요.",
prompt,
detail,
functionSource
);
}
try {
String answer = aiClient.chat(vpdSystemPrompt(), prompt);
return new VpdPolicyExplanation("SUCCESS", answer, prompt, detail, functionSource);
} catch (Exception e) {
return new VpdPolicyExplanation(
"AI_CALL_FAILED",
"AI 호출은 실패했지만 policy/filter 근거는 수집했습니다. 상세: " + e.getMessage(),
prompt,
detail,
functionSource
);
}
}
private String buildExplanationPrompt(VpdPolicyDetail detail, VpdFunctionSource functionSource) {
VpdPolicyView policy = detail.policy();
return """
다음 Oracle VPD policy와 policy function source를 근거로 설명해줘.
Policy Metadata:
- Object: %s
- Policy Group: %s
- Policy Name: %s
- Function: %s
- Statement Types: %s
- Enabled: %s
- Policy Type: %s
- Check Option: %s
- Static Policy: %s
- Long Predicate: %s
DBMS_RLS.ADD_POLICY:
%s
Policy Function Source:
%s
답변 요구사항:
- 한국어로 답변한다.
- 이 policy가 어느 객체의 어떤 SQL 동작에 적용되는지 설명한다.
- filter predicate가 어떤 조건을 만들고 어떤 행이 보이거나 제외되는지 설명한다.
- source에 명시되지 않은 동작은 추측하지 않는다.
- 운영자가 확인할 포인트를 짧게 정리한다.
""".formatted(
policy.objectDisplayName(),
policy.policyGroup(),
policy.policyName(),
policy.functionDisplayName(),
blankToDefault(policy.statementTypes(), "-"),
policy.enabled(),
policy.policyType(),
policy.checkOption(),
policy.staticPolicy(),
policy.longPredicate(),
detail.ddl(),
functionSource.found() ? functionSource.source() : "ALL_SOURCE에서 조회 가능한 source가 없습니다."
);
}
private String vpdSystemPrompt() {
return """
당신은 Oracle ADB VPD(DBMS_RLS), RLS policy function, ORDS 권한 검증을 설명하는 보조자입니다.
제공된 policy metadata와 source만 근거로 설명하고, 없는 정보를 추측하지 마세요.
""";
}
private String buildAddPolicyBlock(VpdPolicyView policy) {
return """
BEGIN

View File

@@ -65,4 +65,22 @@ public class VpdPolicyController {
}
return "fragments/vpd-policy-detail :: detail";
}
@GetMapping("/vpd-policies/policy-explanation")
public String policyExplanation(
@RequestParam String objectOwner,
@RequestParam String objectName,
@RequestParam String policyName,
Model model
) {
try {
model.addAttribute("explanation", vpdPolicyService.explainPolicy(objectOwner, objectName, policyName));
} catch (AppException exception) {
model.addAttribute("errorMessage", exception.getMessage());
} catch (DataAccessException exception) {
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
model.addAttribute("errorMessage", message.message());
}
return "fragments/vpd-policy-explanation :: explanation";
}
}