[Developer] #424 add LLM explanation for VPD policies
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||||
|
|
||||||
|
public record VpdPolicyExplanation(
|
||||||
|
String status,
|
||||||
|
String answer,
|
||||||
|
String prompt,
|
||||||
|
VpdPolicyDetail detail,
|
||||||
|
VpdFunctionSource functionSource
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.cloudhandson.vpdbackoffice.service;
|
|||||||
|
|
||||||
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionSource;
|
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionSource;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyDetail;
|
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.domain.vpd.VpdPolicyView;
|
||||||
import com.cloudhandson.vpdbackoffice.mapper.VpdPolicyMapper;
|
import com.cloudhandson.vpdbackoffice.mapper.VpdPolicyMapper;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -12,9 +13,11 @@ import org.springframework.stereotype.Service;
|
|||||||
public class VpdPolicyService {
|
public class VpdPolicyService {
|
||||||
|
|
||||||
private final VpdPolicyMapper mapper;
|
private final VpdPolicyMapper mapper;
|
||||||
|
private final OpenAiCompatibleClient aiClient;
|
||||||
|
|
||||||
public VpdPolicyService(VpdPolicyMapper mapper) {
|
public VpdPolicyService(VpdPolicyMapper mapper, OpenAiCompatibleClient aiClient) {
|
||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
|
this.aiClient = aiClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<VpdPolicyView> findPolicies() {
|
public List<VpdPolicyView> findPolicies() {
|
||||||
@@ -42,6 +45,90 @@ public class VpdPolicyService {
|
|||||||
return new VpdPolicyDetail(policy, buildAddPolicyBlock(policy));
|
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) {
|
private String buildAddPolicyBlock(VpdPolicyView policy) {
|
||||||
return """
|
return """
|
||||||
BEGIN
|
BEGIN
|
||||||
|
|||||||
@@ -65,4 +65,22 @@ public class VpdPolicyController {
|
|||||||
}
|
}
|
||||||
return "fragments/vpd-policy-detail :: detail";
|
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";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<body>
|
||||||
|
<div th:fragment="explanation">
|
||||||
|
<div class="alert alert-warning mb-0" th:if="${errorMessage}" th:text="${errorMessage}"></div>
|
||||||
|
<div th:if="${explanation}">
|
||||||
|
<div class="section-heading mb-2">
|
||||||
|
<h2>LLM 설명</h2>
|
||||||
|
<span class="badge"
|
||||||
|
th:classappend="${explanation.status() == 'SUCCESS'} ? ' text-bg-success' : ' text-bg-warning'"
|
||||||
|
th:text="${explanation.status()}">SUCCESS</span>
|
||||||
|
</div>
|
||||||
|
<section class="ai-answer">
|
||||||
|
<h3>Answer</h3>
|
||||||
|
<pre th:text="${explanation.answer()}">answer</pre>
|
||||||
|
</section>
|
||||||
|
<div class="probe-exchange-grid">
|
||||||
|
<section class="probe-exchange">
|
||||||
|
<h3>Policy / Filter Prompt</h3>
|
||||||
|
<pre th:text="${explanation.prompt()}">prompt</pre>
|
||||||
|
</section>
|
||||||
|
<section class="probe-exchange">
|
||||||
|
<h3>Policy Function Source</h3>
|
||||||
|
<pre th:if="${explanation.functionSource().found()}" th:text="${explanation.functionSource().source()}"></pre>
|
||||||
|
<pre th:unless="${explanation.functionSource().found()}">ALL_SOURCE에서 조회 가능한 source가 없습니다.</pre>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Options</th>
|
<th>Options</th>
|
||||||
|
<th>Explain</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -70,17 +71,26 @@
|
|||||||
<span class="ms-2">Static: <strong th:text="${policy.staticPolicy()}">NO</strong></span>
|
<span class="ms-2">Static: <strong th:text="${policy.staticPolicy()}">NO</strong></span>
|
||||||
<span class="ms-2">Long: <strong th:text="${policy.longPredicate()}">NO</strong></span>
|
<span class="ms-2">Long: <strong th:text="${policy.longPredicate()}">NO</strong></span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm rw-btn-primary"
|
||||||
|
type="button"
|
||||||
|
th:hx-get="@{/vpd-policies/policy-explanation(objectOwner=${policy.objectOwner()},objectName=${policy.objectName()},policyName=${policy.policyName()})}"
|
||||||
|
th:hx-target="${'#policy-source-' + iter.index}"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
LLM 설명
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7" class="policy-source-cell">
|
<td colspan="8" class="policy-source-cell">
|
||||||
<div th:id="${'policy-source-' + iter.index}" class="text-muted small">
|
<div th:id="${'policy-source-' + iter.index}" class="text-muted small">
|
||||||
Policy 또는 policy function을 클릭하면 상세 내역이 표시됩니다.
|
Policy, policy function, LLM 설명을 클릭하면 상세 내역이 표시됩니다.
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</th:block>
|
</th:block>
|
||||||
<tr th:if="${#lists.isEmpty(policies)}">
|
<tr th:if="${#lists.isEmpty(policies)}">
|
||||||
<td colspan="7" class="text-muted">
|
<td colspan="8" class="text-muted">
|
||||||
등록된 보호 객체에 적용된 VPD policy가 없습니다. 테이블/뷰 관리의 보호 객체 등록 상태와 DB policy 적용 상태를 확인하세요.
|
등록된 보호 객체에 적용된 VPD policy가 없습니다. 테이블/뷰 관리의 보호 객체 등록 상태와 DB policy 적용 상태를 확인하세요.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user