package com.agent.management.rag.capability; import com.agent.management.rag.bridge.RagAiBridgeClient; import com.agent.management.rag.model.RagSourceType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @Service @RequiredArgsConstructor public class RagGovernanceService { private final RagCapabilityProfileService profiles; private final RagAiBridgeClient bridge; private final RagSemanticCatalogService semanticCatalog; private final GraphBusinessSubgraphSelector subgraphs; public Map suggest(RagSourceType type, String sourceId, String description) { RagCapabilityProfile profile = profiles.refresh(type, sourceId); String schema; if (type == RagSourceType.STRUCTURED_DATA) { List tables = semanticCatalog.rank(description, profile, "table:", 12); schema = profile.sqlSchema().toDdl(tables); } else { schema = subgraphs.select(description, profile, profile.graphSchema(), 16, 20).toPromptText(); } Map request = new LinkedHashMap<>(); request.put("sourceType", type.name()); request.put("schemaText", schema); request.put("sourceDescription", description == null ? "" : description); request.put("maxExamples", 8); Map suggestion; try { suggestion = normalize(type, bridge.suggestGovernance(request), profile); suggestion.put("generator", "LLM"); } catch (Exception error) { suggestion = fallback(profile, description); suggestion.put("generator", "SEMANTIC_FALLBACK"); suggestion.put("warning", "模型建议不可用,已按语义目录生成可审核草稿:" + shortMessage(error)); } suggestion.put("profileVersion", profile.version()); suggestion.put("sourceType", type.name()); suggestion.put("sourceId", sourceId); return suggestion; } private Map normalize(RagSourceType type, Map raw, RagCapabilityProfile profile) { Map config = new LinkedHashMap<>(); config.put("retrievalMode", "AUTO_GENERATE"); config.put("generationRules", rules(raw.get("businessRules"), raw.get("rules"))); if (type == RagSourceType.STRUCTURED_DATA) { config.put("allowTextToSql", true); var actualTables = new java.util.HashSet<>(profile.sqlSchema().tables().stream().map(SqlSchemaSnapshot.TableSchema::name).toList()); config.put("allowedTables", strings(raw.containsKey("allowedTables") ? raw.get("allowedTables") : raw.get("tableWhitelist")) .stream().filter(actualTables::contains).toList()); config.put("maxRows", 100); } else { config.put("allowTextToCypher", true); List labels = strings(raw.get("allowedLabels")).stream().filter(profile.graphSchema().labelNames()::contains).toList(); List relationships = strings(raw.get("allowedRelationships")).stream().filter(profile.graphSchema().relationshipTypeNames()::contains).toList(); config.put("allowedLabels", labels); config.put("allowedRelationships", relationships); config.put("allowedProperties", validProperties(raw.get("allowedProperties"), profile, labels, relationships)); config.put("maxDepth", 3); } Map result = new LinkedHashMap<>(); result.put("summary", String.valueOf(raw.getOrDefault("summary", "基于实际 Schema 生成的授权草稿"))); result.put("suggestedConfig", config); result.put("examples", raw.get("examples") instanceof List list ? list : List.of()); return result; } private static List rules(Object primary, Object secondary) { List values = strings(primary); return values.isEmpty() ? strings(secondary) : values; } private static List strings(Object value) { if (value instanceof List list) return list.stream().map(String::valueOf).map(String::trim).filter(item -> !item.isEmpty()).toList(); if (value == null) return List.of(); return java.util.Arrays.stream(String.valueOf(value).split("[\\r\\n;;]+")) .map(String::trim).filter(item -> !item.isEmpty()).toList(); } private static Map> validProperties(Object value, RagCapabilityProfile profile, List labels, List relationships) { if (!(value instanceof Map raw)) return Map.of(); Map> actual = new LinkedHashMap<>(); profile.graphSchema().nodes().forEach(node -> actual.put(node.label(), node.properties().keySet().stream().toList())); profile.graphSchema().relationships().forEach(rel -> actual.put(rel.type(), rel.properties().keySet().stream().toList())); Map> result = new LinkedHashMap<>(); raw.forEach((owner, properties) -> { String name = String.valueOf(owner); if ((!labels.contains(name) && !relationships.contains(name)) || !actual.containsKey(name)) return; result.put(name, strings(properties).stream().filter(actual.get(name)::contains).toList()); }); return result; } private Map fallback(RagCapabilityProfile profile, String description) { Map result = new LinkedHashMap<>(); Map config = new LinkedHashMap<>(); config.put("retrievalMode", "AUTO_GENERATE"); config.put("generationRules", List.of("仅生成只读查询", "只使用已审核的 Schema 元素", "结果必须限制返回规模")); if (profile.sourceType() == RagSourceType.STRUCTURED_DATA) { List tables = semanticCatalog.rank(description, profile, "table:", 8); if (tables.isEmpty()) tables = profile.sqlSchema().tables().stream().limit(8).map(SqlSchemaSnapshot.TableSchema::name).toList(); config.put("allowTextToSql", true); config.put("allowedTables", tables); config.put("maxRows", 100); result.put("examples", tables.stream().limit(4).map(table -> Map.of("question", "查询 " + table + " 的主要记录", "sql", "SELECT * FROM " + table + " LIMIT 20")).toList()); } else { List labels = semanticCatalog.rank(description, profile, "label:", 12); List relationships = semanticCatalog.rank(description, profile, "relationship:", 12); if (labels.isEmpty()) labels = profile.graphSchema().labelNames().stream().limit(12).toList(); List selectedLabels = labels; if (relationships.isEmpty()) relationships = profile.graphSchema().relationships().stream() .filter(rel -> rel.startLabels().stream().anyMatch(selectedLabels::contains) || rel.endLabels().stream().anyMatch(selectedLabels::contains)) .limit(12).map(com.agent.management.rag.graph.GraphSchemaSnapshot.RelationshipSchema::type).distinct().toList(); var subgraph = profile.graphSchema().filter(labels, relationships, Map.of()); Map> properties = new LinkedHashMap<>(); subgraph.nodes().forEach(node -> properties.put(node.label(), node.properties().keySet().stream().toList())); subgraph.relationships().forEach(rel -> properties.put(rel.type(), rel.properties().keySet().stream().toList())); config.put("allowTextToCypher", true); config.put("allowedLabels", subgraph.labelNames()); config.put("allowedRelationships", subgraph.relationshipTypeNames()); config.put("allowedProperties", properties); config.put("maxDepth", 3); result.put("examples", subgraph.nodes().stream().limit(4).map(node -> Map.of("question", "查询 " + node.label() + " 及其关联信息", "cypher", "MATCH (n:`" + node.label() + "`) RETURN n LIMIT 20")).toList()); } result.put("suggestedConfig", config); result.put("summary", "基于实际 Schema 与向量语义相似度生成的授权草稿,应用前需人工审核"); return result; } private static String shortMessage(Exception error) { String value = error.getMessage(); return value == null ? error.getClass().getSimpleName() : value.substring(0, Math.min(value.length(), 180)); } }