guardia-esn/backend/src/main/java/com/zioinfo/esn/rag/RagController.java

99 lines
4.3 KiB
Java

package com.zioinfo.esn.rag;
import com.zioinfo.esn.common.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* ESN 최신 AI 기법(중앙 guardia-rag) 배선 API — 별개 레이어, 테넌트 격리.
*
* <p>기존 {@code /api/ai/*}(AiController) 는 <b>불변</b>. 본 컨트롤러는 중앙 경유 신규 경로만 추가한다.
*
* <ul>
* <li>POST /api/ai/rag/analyze-alarm — ESL/HCore 알람 원인분석·우선순위(/agent+graph+structured)</li>
* <li>POST /api/ai/rag/classify-pos — POS 데이터 자동 분류(/structured, 임계 미만 needsReview)</li>
* <li>POST /api/ai/rag/feedback — 👍/👎 피드백(/rag/feedback, solution=esn 격리)</li>
* <li>GET /api/ai/rag/toggles — 현재 테넌트 기법 토글 스냅샷</li>
* <li>PUT /api/ai/rag/toggles/{key} — 테넌트 토글 변경(MANAGER+, esn_rag_setting 격리)</li>
* </ul>
*
* <p>테넌트는 {@code X-Tenant-Code} 헤더 또는 {@code tenantCode} 바디 필드로 식별(없으면 기본).
*/
@RestController
@RequestMapping("/api/ai/rag")
@RequiredArgsConstructor
public class RagController {
private final RagAiService service;
@PostMapping("/analyze-alarm")
public ApiResponse<Map<String, Object>> analyzeAlarm(
@RequestBody Map<String, Object> req,
@RequestHeader(value = "X-Tenant-Code", required = false) String tenantHeader,
Authentication auth) {
return ApiResponse.ok(service.analyzeAlarm(tenant(tenantHeader, req), req, name(auth)));
}
@PostMapping("/classify-pos")
public ApiResponse<Map<String, Object>> classifyPos(
@RequestBody Map<String, Object> req,
@RequestHeader(value = "X-Tenant-Code", required = false) String tenantHeader,
Authentication auth) {
return ApiResponse.ok(service.classifyPos(tenant(tenantHeader, req), req, name(auth)));
}
@PostMapping("/feedback")
public ApiResponse<Map<String, Object>> feedback(
@RequestBody Map<String, Object> req,
@RequestHeader(value = "X-Tenant-Code", required = false) String tenantHeader,
Authentication auth) {
return ApiResponse.ok(service.feedback(tenant(tenantHeader, req), req, name(auth)));
}
@GetMapping("/toggles")
public ApiResponse<Map<String, Object>> toggles(
@RequestHeader(value = "X-Tenant-Code", required = false) String tenantHeader,
@RequestParam(value = "tenantCode", required = false) String tenantParam) {
String tc = tenantHeader != null ? tenantHeader : tenantParam;
return ApiResponse.ok(service.currentToggles(tc));
}
/**
* 토글 변경 — rag.* 화이트리스트 키만, 테넌트 격리. RBAC(MANAGER+)은 SecurityConfig 통제.
*/
@PutMapping("/toggles/{key}")
public ApiResponse<Map<String, Object>> updateToggle(
@PathVariable String key,
@RequestBody Map<String, Object> req,
@RequestHeader(value = "X-Tenant-Code", required = false) String tenantHeader) {
if (key == null || !key.startsWith("rag.")) {
return ApiResponse.fail("ERR-RAG-400: rag.* 키만 변경 가능");
}
String tc = tenantHeader != null ? tenantHeader : str(req.get("tenantCode"));
Object v = req.get("value");
try {
return ApiResponse.ok(service.updateToggle(tc, key, v == null ? "" : String.valueOf(v)));
} catch (IllegalArgumentException e) {
return ApiResponse.fail(e.getMessage());
}
}
// ── helpers ──────────────────────────────────────────────────────────────
private String tenant(String header, Map<String, Object> req) {
if (header != null && !header.isBlank()) return header;
Object t = req == null ? null : req.get("tenantCode");
return t == null ? null : String.valueOf(t);
}
private String name(Authentication auth) {
return auth == null ? "anon" : auth.getName();
}
private String str(Object o) {
return o == null ? null : String.valueOf(o);
}
}