feat: align DDS with common permission and vector flow
This commit is contained in:
@@ -1,12 +1,37 @@
|
||||
package com.cloudhandson.ddsbackoffice;
|
||||
|
||||
import com.cloudhandson.ddsbackoffice.config.DdsProperties;
|
||||
import com.cloudhandson.vpdbackoffice.VpdBackofficeApplication;
|
||||
import com.cloudhandson.vpdbackoffice.web.DashboardController;
|
||||
import com.cloudhandson.vpdbackoffice.web.LoginController;
|
||||
import com.cloudhandson.vpdbackoffice.web.VectorKnowledgeController;
|
||||
import com.cloudhandson.vpdbackoffice.web.VpdPolicyController;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.FilterType;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableConfigurationProperties(DdsProperties.class)
|
||||
@MapperScan("com.cloudhandson.vpdbackoffice.mapper")
|
||||
@ComponentScan(
|
||||
basePackages = {"com.cloudhandson.ddsbackoffice", "com.cloudhandson.vpdbackoffice"},
|
||||
excludeFilters = @ComponentScan.Filter(
|
||||
type = FilterType.ASSIGNABLE_TYPE,
|
||||
classes = {
|
||||
DdsBackofficeApplication.class,
|
||||
VpdBackofficeApplication.class,
|
||||
com.cloudhandson.ddsbackoffice.config.AppConfig.class,
|
||||
com.cloudhandson.ddsbackoffice.config.SecurityConfig.class,
|
||||
DashboardController.class,
|
||||
LoginController.class,
|
||||
VectorKnowledgeController.class,
|
||||
VpdPolicyController.class
|
||||
}
|
||||
)
|
||||
)
|
||||
public class DdsBackofficeApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.bind.ConstructorBinding;
|
||||
|
||||
@ConfigurationProperties(prefix = "dds")
|
||||
public record DdsProperties(
|
||||
@@ -12,6 +13,7 @@ public record DdsProperties(
|
||||
Duration queryTimeout,
|
||||
String pgObject,
|
||||
String myObject,
|
||||
String vectorObject,
|
||||
Map<String, User> users
|
||||
) {
|
||||
|
||||
@@ -19,11 +21,13 @@ public record DdsProperties(
|
||||
"[A-Za-z][A-Za-z0-9_$#]*(\\.[A-Za-z][A-Za-z0-9_$#]*)*"
|
||||
);
|
||||
|
||||
@ConstructorBinding
|
||||
public DdsProperties {
|
||||
dbUrl = dbUrl == null ? "" : dbUrl.trim();
|
||||
queryTimeout = queryTimeout == null ? Duration.ofSeconds(10) : queryTimeout;
|
||||
pgObject = normalizeObject(pgObject, "ADMIN.V_DDS_CUSTOMERS_PG");
|
||||
myObject = normalizeObject(myObject, "ADMIN.V_DDS_CUSTOMERS_MY");
|
||||
vectorObject = normalizeObject(vectorObject, "ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS");
|
||||
var normalizedUsers = new LinkedHashMap<String, User>();
|
||||
if (users != null) {
|
||||
users.forEach((key, value) -> {
|
||||
@@ -35,6 +39,22 @@ public record DdsProperties(
|
||||
users = Map.copyOf(normalizedUsers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compatibility constructor for small unit tests and older local configs.
|
||||
* The DDS vector view has a safe default and can still be overridden by
|
||||
* {@code DDS_BACKOFFICE_VECTOR_OBJECT}.
|
||||
*/
|
||||
public DdsProperties(
|
||||
String dbUrl,
|
||||
Duration queryTimeout,
|
||||
String pgObject,
|
||||
String myObject,
|
||||
Map<String, User> users
|
||||
) {
|
||||
this(dbUrl, queryTimeout, pgObject, myObject,
|
||||
"ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS", users);
|
||||
}
|
||||
|
||||
public String objectFor(String sourceKey) {
|
||||
if ("PG".equalsIgnoreCase(sourceKey)) {
|
||||
return pgObject;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.cloudhandson.ddsbackoffice.domain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Result of a vector search executed through a DDS END USER connection. */
|
||||
public record DdsVectorSearchResult(
|
||||
String userKey,
|
||||
String userLabel,
|
||||
String objectName,
|
||||
String query,
|
||||
String embeddingMode,
|
||||
String embeddingModel,
|
||||
boolean success,
|
||||
String title,
|
||||
String message,
|
||||
String sessionUser,
|
||||
String endUser,
|
||||
Integer oracleCode,
|
||||
List<Map<String, Object>> rows
|
||||
) {
|
||||
|
||||
public DdsVectorSearchResult {
|
||||
rows = rows == null ? List.of() : List.copyOf(rows);
|
||||
}
|
||||
|
||||
public boolean hasRows() {
|
||||
return !rows.isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,10 @@ public class DdsQueryService {
|
||||
);
|
||||
}
|
||||
|
||||
public String vectorObject() {
|
||||
return properties.vectorObject();
|
||||
}
|
||||
|
||||
public DdsQueryResult query(String userKey, String sourceKey, String searchText, int requestedLimit) {
|
||||
var normalizedUserKey = normalizeUserKey(userKey);
|
||||
var normalizedSourceKey = normalizeSourceKey(sourceKey);
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
package com.cloudhandson.ddsbackoffice.service;
|
||||
|
||||
import com.cloudhandson.ddsbackoffice.config.DdsProperties;
|
||||
import com.cloudhandson.ddsbackoffice.domain.DdsVectorSearchResult;
|
||||
import com.cloudhandson.vpdbackoffice.domain.vector.VectorIngestCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.vector.VectorIngestResult;
|
||||
import com.cloudhandson.vpdbackoffice.domain.vector.VectorKnowledgeSummary;
|
||||
import com.cloudhandson.vpdbackoffice.service.AppException;
|
||||
import com.cloudhandson.vpdbackoffice.service.OpenAiCompatibleClient;
|
||||
import com.cloudhandson.vpdbackoffice.service.VectorKnowledgeService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Pattern;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* DDS version of the common knowledge flow.
|
||||
*
|
||||
* Ingestion still writes the shared chunk/tag store. Search deliberately does
|
||||
* not call the VPD ORDS probe: it opens a DDS END USER connection and lets the
|
||||
* DATA ROLE/DATA GRANT on the DDS-only view decide which chunks are visible.
|
||||
*/
|
||||
@Service
|
||||
public class DdsVectorKnowledgeService {
|
||||
|
||||
private static final String DEMO_MODE = "DEMO";
|
||||
private static final String AI_MODE = "AI";
|
||||
private static final Pattern USER_KEY = Pattern.compile("[a-z0-9_-]{1,40}");
|
||||
|
||||
private final VectorKnowledgeService commonVectorService;
|
||||
private final OpenAiCompatibleClient embeddingClient;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final DdsProperties properties;
|
||||
|
||||
public DdsVectorKnowledgeService(
|
||||
VectorKnowledgeService commonVectorService,
|
||||
OpenAiCompatibleClient embeddingClient,
|
||||
ObjectMapper objectMapper,
|
||||
DdsProperties properties
|
||||
) {
|
||||
this.commonVectorService = commonVectorService;
|
||||
this.embeddingClient = embeddingClient;
|
||||
this.objectMapper = objectMapper;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public VectorKnowledgeSummary summary() {
|
||||
return commonVectorService.summary();
|
||||
}
|
||||
|
||||
public boolean aiEmbeddingConfigured() {
|
||||
return commonVectorService.aiEmbeddingConfigured();
|
||||
}
|
||||
|
||||
public VectorIngestResult ingest(VectorIngestCommand command) {
|
||||
return commonVectorService.ingest(command);
|
||||
}
|
||||
|
||||
public DdsVectorSearchResult search(String userKey, String query, int requestedLimit, String embeddingMode) {
|
||||
String normalizedUserKey = normalizeUserKey(userKey);
|
||||
var user = properties.users().get(normalizedUserKey);
|
||||
String userLabel = user == null || user.label() == null || user.label().isBlank()
|
||||
? normalizedUserKey : user.label();
|
||||
String normalizedQuery = required(query, "검색 질문");
|
||||
String mode = normalizeMode(embeddingMode);
|
||||
|
||||
if (user == null) {
|
||||
return failure(normalizedUserKey, userLabel, normalizedQuery, mode,
|
||||
"DDS END USER를 찾을 수 없습니다.", "설정에 등록된 DDS 보안 주체만 선택할 수 있습니다.", null);
|
||||
}
|
||||
if (properties.dbUrl().isBlank()) {
|
||||
return failure(normalizedUserKey, userLabel, normalizedQuery, mode,
|
||||
"DDS 데이터베이스 연결 정보가 없습니다.",
|
||||
"DDS_BACKOFFICE_DB_URL 또는 BACKOFFICE_DB_URL을 설정하세요.", null);
|
||||
}
|
||||
if (!user.configured()) {
|
||||
return failure(normalizedUserKey, userLabel, normalizedQuery, mode,
|
||||
"DDS END USER 자격증명이 설정되지 않았습니다.",
|
||||
"이 인스턴스의 환경 변수에 해당 END USER 비밀번호를 설정하세요.", null);
|
||||
}
|
||||
|
||||
int limit = Math.max(1, Math.min(requestedLimit, 100));
|
||||
int timeoutSeconds = timeoutSeconds(properties.queryTimeout());
|
||||
String vectorJson = json(embed(normalizedQuery, mode));
|
||||
Properties connectionProperties = new Properties();
|
||||
connectionProperties.setProperty("user", jdbcUsername(user.username()));
|
||||
connectionProperties.setProperty("password", user.password());
|
||||
connectionProperties.setProperty("oracle.net.CONNECT_TIMEOUT", String.valueOf(timeoutSeconds * 1000));
|
||||
connectionProperties.setProperty("oracle.jdbc.ReadTimeout", String.valueOf(timeoutSeconds * 1000));
|
||||
|
||||
String sql = "SELECT chunk_id, document_id, chunk_no, title, chunk_text, source_uri, tech_tag, score "
|
||||
+ "FROM (SELECT d.chunk_id, d.document_id, d.chunk_no, d.title, d.chunk_text, d.source_uri, "
|
||||
+ "d.tech_tag, VECTOR_DISTANCE(d.embedding, TO_VECTOR(?), COSINE) AS score "
|
||||
+ "FROM " + properties.vectorObject() + " d "
|
||||
+ "WHERE d.embedding IS NOT NULL ORDER BY score) ranked_chunks WHERE ROWNUM <= ?";
|
||||
|
||||
try (Connection connection = java.sql.DriverManager.getConnection(properties.dbUrl(), connectionProperties)) {
|
||||
connection.setReadOnly(true);
|
||||
String sessionUser = readSingleValue(connection,
|
||||
"SELECT SYS_CONTEXT('USERENV', 'SESSION_USER') FROM dual", timeoutSeconds);
|
||||
String endUser = readSingleValue(connection,
|
||||
"SELECT ORA_END_USER_CONTEXT.username FROM dual", timeoutSeconds);
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setQueryTimeout(timeoutSeconds);
|
||||
statement.setString(1, vectorJson);
|
||||
statement.setInt(2, limit);
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
while (resultSet.next()) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("CHUNK_ID", resultSet.getLong("chunk_id"));
|
||||
row.put("DOCUMENT_ID", resultSet.getString("document_id"));
|
||||
row.put("CHUNK_NO", resultSet.getInt("chunk_no"));
|
||||
row.put("TITLE", resultSet.getString("title"));
|
||||
row.put("CHUNK_TEXT", resultSet.getString("chunk_text"));
|
||||
row.put("SOURCE_URI", resultSet.getString("source_uri"));
|
||||
row.put("TECH_TAG", resultSet.getString("tech_tag"));
|
||||
row.put("SCORE", resultSet.getObject("score"));
|
||||
rows.add(row);
|
||||
}
|
||||
}
|
||||
String message = rows.isEmpty()
|
||||
? "DDS DATA GRANT를 통과한 검색 단위가 없습니다."
|
||||
: rows.size() + "개 검색 단위가 DDS DATA GRANT를 통과했습니다.";
|
||||
return new DdsVectorSearchResult(
|
||||
normalizedUserKey, userLabel, properties.vectorObject(), normalizedQuery, mode,
|
||||
embeddingModel(mode), true, "DDS 권한으로 검색했습니다.", message,
|
||||
sessionUser, endUser, null, rows);
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
return failure(normalizedUserKey, userLabel, normalizedQuery, mode,
|
||||
classifyTitle(exception), classifyMessage(exception), oracleCode(exception));
|
||||
}
|
||||
}
|
||||
|
||||
private DdsVectorSearchResult failure(
|
||||
String userKey,
|
||||
String userLabel,
|
||||
String query,
|
||||
String mode,
|
||||
String title,
|
||||
String message,
|
||||
Integer oracleCode
|
||||
) {
|
||||
return new DdsVectorSearchResult(
|
||||
userKey, userLabel, properties.vectorObject(), query, mode, embeddingModel(mode),
|
||||
false, title, message, null, null, oracleCode, List.of());
|
||||
}
|
||||
|
||||
private String readSingleValue(Connection connection, String sql, int timeoutSeconds)
|
||||
throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setQueryTimeout(timeoutSeconds);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
return resultSet.next() ? resultSet.getString(1) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Double> embed(String input, String mode) {
|
||||
if (AI_MODE.equals(mode)) {
|
||||
return embeddingClient.embedding(input);
|
||||
}
|
||||
return demoEmbedding(input);
|
||||
}
|
||||
|
||||
private List<Double> demoEmbedding(String input) {
|
||||
double[] vector = new double[4];
|
||||
String normalized = input.toLowerCase(Locale.ROOT);
|
||||
addIfContains(vector, normalized, 0, "spring", "boot", "java", "security");
|
||||
addIfContains(vector, normalized, 1, "oracle", "vpd", "policy", "database", "db");
|
||||
addIfContains(vector, normalized, 2, "ords", "rest", "handler", "http", "api");
|
||||
addIfContains(vector, normalized, 3, "mcp", "tool", "권한", "tag", "태그");
|
||||
for (String token : normalized.split("[^a-z0-9가-힣]+")) {
|
||||
if (token.length() >= 3) {
|
||||
vector[Math.floorMod(token.hashCode(), vector.length)] += 0.03;
|
||||
}
|
||||
}
|
||||
double length = 0;
|
||||
for (double value : vector) {
|
||||
length += value * value;
|
||||
}
|
||||
if (length == 0) {
|
||||
vector[0] = 1;
|
||||
length = 1;
|
||||
}
|
||||
double scale = Math.sqrt(length);
|
||||
List<Double> result = new ArrayList<>(vector.length);
|
||||
for (double value : vector) {
|
||||
result.add(value / scale);
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private void addIfContains(double[] vector, String input, int index, String... terms) {
|
||||
for (String term : terms) {
|
||||
if (input.contains(term)) {
|
||||
vector[index] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeUserKey(String value) {
|
||||
String normalized = value == null ? "" : value.trim().toLowerCase(Locale.ROOT);
|
||||
if (!USER_KEY.matcher(normalized).matches()) {
|
||||
throw new IllegalArgumentException("지원하지 않는 DDS END USER 선택값입니다.");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String normalizeMode(String value) {
|
||||
String normalized = value == null ? DEMO_MODE : value.trim().toUpperCase(Locale.ROOT);
|
||||
if (!DEMO_MODE.equals(normalized) && !AI_MODE.equals(normalized)) {
|
||||
throw new AppException("임베딩 방식은 로컬 임베딩 또는 AI 임베딩만 사용할 수 있습니다.");
|
||||
}
|
||||
if (AI_MODE.equals(normalized) && !embeddingClient.embeddingConfigured()) {
|
||||
throw new AppException("AI 임베딩이 설정되지 않았습니다. 설정에서 임베딩 모델/API key를 추가하세요.");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String embeddingModel(String mode) {
|
||||
return AI_MODE.equals(mode) ? embeddingClient.embeddingModelName() : "로컬 임베딩(개발용)";
|
||||
}
|
||||
|
||||
private String json(List<Double> values) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(values);
|
||||
} catch (Exception exception) {
|
||||
throw new AppException("임베딩 JSON 생성에 실패했습니다: " + exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static int timeoutSeconds(Duration duration) {
|
||||
return Math.max(1, (int) Math.ceil(duration.toMillis() / 1000.0));
|
||||
}
|
||||
|
||||
private static String jdbcUsername(String username) {
|
||||
String normalized = username == null ? "" : username.trim();
|
||||
if (normalized.length() >= 2 && normalized.startsWith("\"") && normalized.endsWith("\"")) {
|
||||
return normalized;
|
||||
}
|
||||
if (!normalized.isEmpty() && normalized.equals(normalized.toLowerCase(Locale.ROOT))) {
|
||||
return "\"" + normalized.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static String required(String value, String label) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new AppException(label + "을 입력하세요.");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private static Integer oracleCode(SQLException exception) {
|
||||
SQLException current = exception;
|
||||
while (current != null) {
|
||||
if (current.getErrorCode() != 0) {
|
||||
return Math.abs(current.getErrorCode());
|
||||
}
|
||||
current = current.getNextException();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String flattenedMessage(SQLException exception) {
|
||||
StringBuilder messages = new StringBuilder();
|
||||
SQLException current = exception;
|
||||
while (current != null) {
|
||||
if (messages.length() > 0) {
|
||||
messages.append("; ");
|
||||
}
|
||||
messages.append(Objects.toString(current.getMessage(), ""));
|
||||
current = current.getNextException();
|
||||
}
|
||||
return messages.toString().replaceAll("\\s+", " ").trim();
|
||||
}
|
||||
|
||||
private static String classifyTitle(SQLException exception) {
|
||||
String message = flattenedMessage(exception).toUpperCase(Locale.ROOT);
|
||||
if (message.contains("ORA-00942")) {
|
||||
return "이 DDS END USER에게 지식자료가 허용되지 않았습니다.";
|
||||
}
|
||||
if (message.contains("ORA-01017")) {
|
||||
return "DDS END USER 인증에 실패했습니다.";
|
||||
}
|
||||
if (message.contains("CONNECTION") || message.contains("ORA-12154")
|
||||
|| message.contains("ORA-12514") || message.contains("IO ERROR")) {
|
||||
return "DDS 데이터베이스에 연결할 수 없습니다.";
|
||||
}
|
||||
return "DDS 벡터 검색 중 데이터베이스 오류가 발생했습니다.";
|
||||
}
|
||||
|
||||
private static String classifyMessage(SQLException exception) {
|
||||
String message = flattenedMessage(exception);
|
||||
String upper = message.toUpperCase(Locale.ROOT);
|
||||
if (upper.contains("ORA-00942")) {
|
||||
return "END USER의 DATA ROLE에 이 DDS 벡터 VIEW를 대상으로 한 DATA GRANT가 없습니다. default deny 결과입니다.";
|
||||
}
|
||||
if (upper.contains("ORA-01017")) {
|
||||
return "서버에 등록된 DDS END USER 비밀번호를 확인하세요.";
|
||||
}
|
||||
return "DDS 벡터 VIEW, DATA ROLE/GRANT, DB 접속 정보를 확인하세요.";
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@ package com.cloudhandson.ddsbackoffice.web;
|
||||
|
||||
import com.cloudhandson.ddsbackoffice.domain.DdsQueryResult;
|
||||
import com.cloudhandson.ddsbackoffice.service.DdsQueryService;
|
||||
import com.cloudhandson.vpdbackoffice.service.PermissionService;
|
||||
import java.util.List;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -12,12 +15,29 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
public class DdsController {
|
||||
|
||||
private final DdsQueryService service;
|
||||
private final PermissionService permissionService;
|
||||
|
||||
public DdsController(DdsQueryService service) {
|
||||
public DdsController(DdsQueryService service, PermissionService permissionService) {
|
||||
this.service = service;
|
||||
this.permissionService = permissionService;
|
||||
}
|
||||
|
||||
@GetMapping({"/", "/dds"})
|
||||
@GetMapping("/")
|
||||
public String home(Model model) {
|
||||
try {
|
||||
model.addAttribute("roles", permissionService.findRoles());
|
||||
model.addAttribute("permissions", permissionService.findPermissionViews());
|
||||
} catch (DataAccessException exception) {
|
||||
model.addAttribute("roles", List.of());
|
||||
model.addAttribute("permissions", List.of());
|
||||
model.addAttribute("runtimeError", exception.getMessage());
|
||||
}
|
||||
model.addAttribute("vectorObject", service.vectorObject());
|
||||
model.addAttribute("configuredUserCount", service.users().stream().filter(user -> user.configured()).count());
|
||||
return "dds-home";
|
||||
}
|
||||
|
||||
@GetMapping("/dds")
|
||||
public String page(Model model) {
|
||||
addOptions(model, "both", "PG", null);
|
||||
return "dds";
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.cloudhandson.ddsbackoffice.web;
|
||||
|
||||
import com.cloudhandson.ddsbackoffice.config.DdsProperties;
|
||||
import com.cloudhandson.ddsbackoffice.service.DdsQueryService;
|
||||
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionView;
|
||||
import com.cloudhandson.vpdbackoffice.service.PermissionService;
|
||||
import java.util.List;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.servlet.view.RedirectView;
|
||||
|
||||
/** DDS equivalent of the VPD protection-connection step. */
|
||||
@Controller
|
||||
public class DdsProtectionController {
|
||||
|
||||
private static final String MANAGEMENT_OBJECT = "CB_VECTOR_SEARCH_DOCUMENTS";
|
||||
|
||||
private final PermissionService permissionService;
|
||||
private final DdsProperties properties;
|
||||
private final DdsQueryService queryService;
|
||||
|
||||
public DdsProtectionController(
|
||||
PermissionService permissionService,
|
||||
DdsProperties properties,
|
||||
DdsQueryService queryService
|
||||
) {
|
||||
this.permissionService = permissionService;
|
||||
this.properties = properties;
|
||||
this.queryService = queryService;
|
||||
}
|
||||
|
||||
@GetMapping("/vpd-policies")
|
||||
public String page(Model model) {
|
||||
model.addAttribute("ddsVectorObject", properties.vectorObject());
|
||||
model.addAttribute("managementObject", MANAGEMENT_OBJECT);
|
||||
model.addAttribute("ddsUsers", queryService.users());
|
||||
try {
|
||||
List<PermissionView> permissions = permissionService.findPermissionViews().stream()
|
||||
.filter(permission -> MANAGEMENT_OBJECT.equalsIgnoreCase(permission.objectName()))
|
||||
.toList();
|
||||
model.addAttribute("permissions", permissions);
|
||||
} catch (DataAccessException exception) {
|
||||
model.addAttribute("permissions", List.of());
|
||||
model.addAttribute("runtimeError", exception.getMessage());
|
||||
}
|
||||
return "vpd-policies";
|
||||
}
|
||||
|
||||
@GetMapping("/vpd-filter-policies")
|
||||
public RedirectView filterPolicies() {
|
||||
return new RedirectView("/vpd-policies");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.cloudhandson.ddsbackoffice.web;
|
||||
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
@ControllerAdvice
|
||||
public class DdsTrackModelAdvice {
|
||||
|
||||
@ModelAttribute("backofficeTrack")
|
||||
public String backofficeTrack() {
|
||||
return "DDS";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.cloudhandson.ddsbackoffice.web;
|
||||
|
||||
import com.cloudhandson.ddsbackoffice.service.DdsQueryService;
|
||||
import com.cloudhandson.ddsbackoffice.service.DdsVectorKnowledgeService;
|
||||
import com.cloudhandson.vpdbackoffice.domain.vector.VectorIngestCommand;
|
||||
import com.cloudhandson.vpdbackoffice.service.AppException;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
@Controller
|
||||
public class DdsVectorKnowledgeController {
|
||||
|
||||
private final DdsVectorKnowledgeService service;
|
||||
private final DdsQueryService ddsQueryService;
|
||||
|
||||
public DdsVectorKnowledgeController(
|
||||
DdsVectorKnowledgeService service,
|
||||
DdsQueryService ddsQueryService
|
||||
) {
|
||||
this.service = service;
|
||||
this.ddsQueryService = ddsQueryService;
|
||||
}
|
||||
|
||||
@GetMapping("/vector-knowledge")
|
||||
public String page(Model model) {
|
||||
populatePage(model);
|
||||
return "vector-knowledge";
|
||||
}
|
||||
|
||||
@PostMapping("/vector-knowledge/ingest")
|
||||
public String ingest(
|
||||
@RequestParam String documentId,
|
||||
@RequestParam String title,
|
||||
@RequestParam(required = false, defaultValue = "") String sourceUri,
|
||||
@RequestParam String content,
|
||||
@RequestParam String techTags,
|
||||
@RequestParam(defaultValue = "600") int chunkSize,
|
||||
@RequestParam(defaultValue = "DEMO") String embeddingMode,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
var result = service.ingest(new VectorIngestCommand(
|
||||
documentId, title, sourceUri, content, techTags, chunkSize, embeddingMode));
|
||||
redirectAttributes.addFlashAttribute("ingestResult", result);
|
||||
redirectAttributes.addFlashAttribute("message",
|
||||
result.chunkCount() + "개 청크를 저장했습니다. DDS 권한 평가용 태그 "
|
||||
+ result.tagCount() + "개가 연결되었습니다.");
|
||||
} catch (Exception exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
return "redirect:/vector-knowledge";
|
||||
}
|
||||
|
||||
@PostMapping("/vector-knowledge/search")
|
||||
public String search(
|
||||
@RequestParam String userKey,
|
||||
@RequestParam String query,
|
||||
@RequestParam(defaultValue = "10") int limit,
|
||||
@RequestParam(defaultValue = "DEMO") String embeddingMode,
|
||||
Model model
|
||||
) {
|
||||
try {
|
||||
model.addAttribute("searchResult", service.search(userKey, query, limit, embeddingMode));
|
||||
} catch (AppException | IllegalArgumentException exception) {
|
||||
model.addAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
return "fragments/dds-vector-search-result :: result";
|
||||
}
|
||||
|
||||
private void populatePage(Model model) {
|
||||
try {
|
||||
model.addAttribute("summary", service.summary());
|
||||
model.addAttribute("aiEmbeddingConfigured", service.aiEmbeddingConfigured());
|
||||
} catch (DataAccessException exception) {
|
||||
model.addAttribute("summary", null);
|
||||
model.addAttribute("runtimeError", exception.getMessage());
|
||||
model.addAttribute("aiEmbeddingConfigured", false);
|
||||
}
|
||||
model.addAttribute("ddsUsers", ddsQueryService.users());
|
||||
model.addAttribute("ddsVectorObject", ddsQueryService.vectorObject());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user