feat: 최신 AI 기법 적용 — 중앙 guardia-rag 경유 배선(근거검증·구조화출력·에이전틱) + 기법 토글
This commit is contained in:
parent
0c5436291c
commit
23c1542c0a
@ -57,6 +57,9 @@ public class SecurityConfig {
|
||||
.requestMatchers("/api/admin/audit").hasAnyRole("SUPERADMIN", "MANAGER")
|
||||
.requestMatchers("/api/admin/**").hasRole("SUPERADMIN")
|
||||
|
||||
// RAG 기법 토글 변경 — MANAGER 이상(운영자 전용). 분석 트리거(POST)는 아래 WORKER+ 규칙 적용.
|
||||
.requestMatchers(HttpMethod.PUT, "/api/mes/rag/toggles/**").hasAnyRole("SUPERADMIN", "MANAGER")
|
||||
|
||||
// 변경(실적·검사·입출고·재고이동) — WORKER 이상 (기준정보/마감/승인은 서비스에서 MANAGER+ 가드)
|
||||
.requestMatchers(HttpMethod.POST, "/api/mes/**").hasAnyRole("SUPERADMIN", "MANAGER", "WORKER")
|
||||
.requestMatchers(HttpMethod.PUT, "/api/mes/**").hasAnyRole("SUPERADMIN", "MANAGER", "WORKER")
|
||||
|
||||
193
backend/src/main/java/com/zioinfo/mes/rag/RagClient.java
Normal file
193
backend/src/main/java/com/zioinfo/mes/rag/RagClient.java
Normal file
@ -0,0 +1,193 @@
|
||||
package com.zioinfo.mes.rag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 얇은 중앙 guardia-rag REST 클라이언트 (MES 전용).
|
||||
*
|
||||
* <p><b>핵심 설계</b>: MES 는 LangChain·검색·에이전틱 로직을 Java 에 재구현하지 않는다. 중앙 Python
|
||||
* guardia-rag 의 검색·에이전틱·구조화·검증을 REST 로 호출하기만 한다. 미가용/타임아웃/RAM 부족 시
|
||||
* 절대 예외를 전파하지 않고 {@code degraded:true} 가 표시된 빈/폴백 결과를 돌려준다(서비스 계층이
|
||||
* MES 결정론 로컬 폴백 수행).
|
||||
*
|
||||
* <p><b>보안 불변</b>: 온프레미스 전용. 응답에서 자격증명/거래처·인사 PII/스택트레이스 미노출. 오류는 요약만 로깅.
|
||||
*
|
||||
* <p>호출 계약(중앙, 8020):
|
||||
* <ul>
|
||||
* <li>POST /rag/answer — 검색(retrieval_mode)+근거생성+인용+guardrail</li>
|
||||
* <li>POST /rag/agent — 에이전틱 tool-use(멀티스텝 추론, step 상한)</li>
|
||||
* <li>POST /rag/structured — format:json 결정론 JSON(스키마 강제)</li>
|
||||
* <li>POST /rag/feedback — 👍/👎 피드백(solution=mes 격리)</li>
|
||||
* <li>GET /rag/trust/settings — 가용성 프로브</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RagClient {
|
||||
|
||||
private final WebClient.Builder builder;
|
||||
private final RagProperties props;
|
||||
|
||||
public RagClient(WebClient.Builder builder, RagProperties props) {
|
||||
this.builder = builder;
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
/** 중앙 RAG 사용 가능 여부(설정 enabled + trust/settings 도달). */
|
||||
public boolean available() {
|
||||
if (!props.isEnabled()) return false;
|
||||
try {
|
||||
builder.baseUrl(props.getBaseUrl()).build()
|
||||
.get().uri("/rag/trust/settings?solution=" + props.getSolution())
|
||||
.retrieve().bodyToMono(String.class)
|
||||
.timeout(Duration.ofSeconds(3)).block();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /rag/structured — 스키마 강제 결정론 JSON.
|
||||
*
|
||||
* @return {@code {data:{...}, valid, fallback_used, degraded}} 형태. 미가용 시 {@code degraded:true}.
|
||||
*/
|
||||
public Map<String, Object> structured(String prompt, Map<String, Object> schema,
|
||||
String model, double temperature) {
|
||||
if (!props.isEnabled()) return degraded("rag_disabled");
|
||||
try {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("solution", props.getSolution());
|
||||
body.put("prompt", prompt);
|
||||
if (schema != null) body.put("schema", schema);
|
||||
if (model != null) body.put("model", model);
|
||||
body.put("temperature", temperature);
|
||||
body.put("max_retries", 1);
|
||||
Map<String, Object> res = post("/rag/structured", body);
|
||||
return res != null ? res : degraded("rag_no_response");
|
||||
} catch (Exception e) {
|
||||
log.warn("RAG /structured 일시 불가 — MES 폴백: {}", summarize(e));
|
||||
return degraded("rag_unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /rag/answer — 검색(retrieval_mode)+생성+검증+guardrail.
|
||||
*
|
||||
* @return {@code {answer, grounded, faithfulness, citations[], guardrail{...}, sources[], answer_id, degraded}}.
|
||||
* 미가용 시 {@code degraded:true}.
|
||||
*/
|
||||
public Map<String, Object> answer(String query, String retrievalMode, boolean rerank,
|
||||
int topK, String model, boolean verify) {
|
||||
if (!props.isEnabled()) return degraded("rag_disabled");
|
||||
try {
|
||||
Map<String, Object> options = new LinkedHashMap<>();
|
||||
options.put("rerank", rerank);
|
||||
options.put("stream", false);
|
||||
options.put("verify", verify);
|
||||
if (model != null) options.put("model", model);
|
||||
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("solution", props.getSolution());
|
||||
body.put("query", query);
|
||||
body.put("retrieval_mode", retrievalMode == null ? "vector" : retrievalMode);
|
||||
body.put("top_k", topK <= 0 ? 5 : topK);
|
||||
body.put("options", options);
|
||||
Map<String, Object> res = post("/rag/answer", body);
|
||||
return res != null ? res : degraded("rag_no_response");
|
||||
} catch (Exception e) {
|
||||
log.warn("RAG /answer 일시 불가 — MES 폴백: {}", summarize(e));
|
||||
return degraded("rag_unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /rag/agent — 에이전틱 tool-use(멀티스텝 추론). MES 도메인 조회 매퍼를 도구로 노출해
|
||||
* 조회→집계→해석을 수행한다. {@code maxSteps} 상한으로 폭주/RAM 위협 차단.
|
||||
*
|
||||
* @param tools 읽기전용 조회 도구 정의 목록(이름·설명·파라미터 스키마). 쓰기/SSH 도구 금지.
|
||||
* @return {@code {answer, steps[], tool_calls[], structured?, answer_id, degraded}}. 미가용 시 {@code degraded:true}.
|
||||
*/
|
||||
public Map<String, Object> agent(String task, Object tools, String retrievalMode,
|
||||
int maxSteps, String model, Map<String, Object> structuredSchema) {
|
||||
if (!props.isEnabled()) return degraded("rag_disabled");
|
||||
try {
|
||||
Map<String, Object> options = new LinkedHashMap<>();
|
||||
options.put("max_steps", maxSteps <= 0 ? 4 : Math.min(maxSteps, 8)); // 상한 강제
|
||||
options.put("stream", false);
|
||||
if (model != null) options.put("model", model);
|
||||
if (retrievalMode != null) options.put("retrieval_mode", retrievalMode);
|
||||
if (structuredSchema != null) options.put("schema", structuredSchema); // tool-use + 구조화 결합
|
||||
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("solution", props.getSolution());
|
||||
body.put("task", task);
|
||||
if (tools != null) body.put("tools", tools);
|
||||
body.put("options", options);
|
||||
Map<String, Object> res = post("/rag/agent", body);
|
||||
return res != null ? res : degraded("rag_no_response");
|
||||
} catch (Exception e) {
|
||||
log.warn("RAG /agent 일시 불가 — MES 폴백: {}", summarize(e));
|
||||
return degraded("rag_unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /rag/feedback — 👍/👎 피드백(solution=mes 격리).
|
||||
*
|
||||
* @return {@code {feedback_id, stored}} 또는 미가용 시 {@code {stored:false, degraded:true}}.
|
||||
*/
|
||||
public Map<String, Object> feedback(String answerId, String query, String answer,
|
||||
String verdict, String correction, String userRef) {
|
||||
if (!props.isEnabled()) return Map.of("stored", false, "degraded", true);
|
||||
try {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("solution", props.getSolution());
|
||||
if (answerId != null) body.put("answer_id", answerId);
|
||||
if (query != null) body.put("query", query);
|
||||
if (answer != null) body.put("answer", answer);
|
||||
body.put("verdict", verdict);
|
||||
if (correction != null) body.put("correction", correction);
|
||||
if (userRef != null) body.put("user_ref", userRef);
|
||||
Map<String, Object> res = post("/rag/feedback", body);
|
||||
return res != null ? res : Map.of("stored", false, "degraded", true);
|
||||
} catch (Exception e) {
|
||||
log.warn("RAG /feedback 일시 불가: {}", summarize(e));
|
||||
return Map.of("stored", false, "degraded", true);
|
||||
}
|
||||
}
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> post(String path, Map<String, Object> body) {
|
||||
return builder.baseUrl(props.getBaseUrl()).build()
|
||||
.post().uri(path)
|
||||
.header("X-Solution-Key", props.getSolution())
|
||||
.bodyValue(body)
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.timeout(Duration.ofMillis(props.getTimeoutMs()))
|
||||
.map(m -> (Map<String, Object>) m)
|
||||
.block();
|
||||
}
|
||||
|
||||
private Map<String, Object> degraded(String reason) {
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("degraded", true);
|
||||
out.put("degraded_reason", reason);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 스택트레이스 미노출: 메시지 1줄만 요약. */
|
||||
private String summarize(Exception e) {
|
||||
String m = e.getMessage();
|
||||
if (m == null) return e.getClass().getSimpleName();
|
||||
return m.length() > 160 ? m.substring(0, 160) : m;
|
||||
}
|
||||
}
|
||||
71
backend/src/main/java/com/zioinfo/mes/rag/RagController.java
Normal file
71
backend/src/main/java/com/zioinfo/mes/rag/RagController.java
Normal file
@ -0,0 +1,71 @@
|
||||
package com.zioinfo.mes.rag;
|
||||
|
||||
import com.zioinfo.mes.admin.SettingService;
|
||||
import com.zioinfo.mes.common.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MES 최신 AI 기법(중앙 guardia-rag) 배선 API — 별개 레이어.
|
||||
*
|
||||
* <p>기존 {@code /api/mes/ai/*}(AiController, 8개 제조 AI)는 <b>불변</b>. 본 컨트롤러는 중앙 경유
|
||||
* 신규 경로만 추가한다. RBAC 은 {@code SecurityConfig} 가 통제(POST=Worker+, 토글 PUT=Manager+).
|
||||
*
|
||||
* <ul>
|
||||
* <li>POST /api/mes/rag/defect-analysis — 불량 RCA + SPC 이상감지(/agent+/structured, SPC 수치는 결정론)</li>
|
||||
* <li>POST /api/mes/rag/predict-analysis — 설비 예지보전 + 수요/생산 예측(/agent, 수치는 결정론 베이스라인)</li>
|
||||
* <li>POST /api/mes/rag/feedback — 👍/👎 피드백(/rag/feedback, solution=mes 격리)</li>
|
||||
* <li>GET /api/mes/rag/toggles — 현재 기법 토글 스냅샷</li>
|
||||
* <li>PUT /api/mes/rag/toggles/{key} — 토글 변경(Manager+, mes_setting 격리 저장)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/mes/rag")
|
||||
@RequiredArgsConstructor
|
||||
public class RagController {
|
||||
|
||||
private final RagMesService service;
|
||||
private final SettingService settingService;
|
||||
|
||||
@PostMapping("/defect-analysis")
|
||||
public ApiResponse<Map<String, Object>> defectAnalysis(@RequestBody Map<String, Object> req, Authentication auth) {
|
||||
return ApiResponse.ok(service.defectAnalysis(req, name(auth)));
|
||||
}
|
||||
|
||||
@PostMapping("/predict-analysis")
|
||||
public ApiResponse<Map<String, Object>> predictAnalysis(@RequestBody Map<String, Object> req, Authentication auth) {
|
||||
return ApiResponse.ok(service.predictAnalysis(req, name(auth)));
|
||||
}
|
||||
|
||||
@PostMapping("/feedback")
|
||||
public ApiResponse<Map<String, Object>> feedback(@RequestBody Map<String, Object> req, Authentication auth) {
|
||||
return ApiResponse.ok(service.feedback(req, name(auth)));
|
||||
}
|
||||
|
||||
@GetMapping("/toggles")
|
||||
public ApiResponse<Map<String, Object>> toggles() {
|
||||
return ApiResponse.ok(service.currentToggles());
|
||||
}
|
||||
|
||||
/**
|
||||
* 토글 변경 — 기존 mes_setting upsert(감사로그 자동 기록) 재사용. RBAC 은 SecurityConfig 통제.
|
||||
* 허용 키는 {@code rag.*} 네임스페이스로 한정(다른 설정 보호).
|
||||
*/
|
||||
@PutMapping("/toggles/{key}")
|
||||
public ApiResponse<Map<String, Object>> updateToggle(@PathVariable String key,
|
||||
@RequestBody Map<String, Object> req) {
|
||||
if (key == null || !key.startsWith("rag.")) {
|
||||
return ApiResponse.fail("ERR-RAG-400: rag.* 키만 변경 가능");
|
||||
}
|
||||
Object v = req.get("value");
|
||||
settingService.update(key, v == null ? "" : String.valueOf(v));
|
||||
return ApiResponse.ok(service.currentToggles());
|
||||
}
|
||||
|
||||
private String name(Authentication auth) {
|
||||
return auth == null ? "anon" : auth.getName();
|
||||
}
|
||||
}
|
||||
308
backend/src/main/java/com/zioinfo/mes/rag/RagMesService.java
Normal file
308
backend/src/main/java/com/zioinfo/mes/rag/RagMesService.java
Normal file
@ -0,0 +1,308 @@
|
||||
package com.zioinfo.mes.rag;
|
||||
|
||||
import com.zioinfo.mes.ai.AiService;
|
||||
import com.zioinfo.mes.rag.RagToggleService.RagToggles;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MES 대표 AI 기능 RAG 배선 레이어(순증·비파괴).
|
||||
*
|
||||
* <p>기존 {@link AiService}(8개 제조 AI, Ollama + 결정론 Java 폴백)는 <b>불변</b>. 본 서비스는
|
||||
* 중앙 guardia-rag 를 경유하는 <b>별개 레이어</b>로, 두 대표 기능을 고급 기법으로 전환한다:
|
||||
* <ol>
|
||||
* <li><b>품질 분석(defectAnalysis)</b> — 불량 원인분석 + SPC 이상감지.
|
||||
* SPC 수치(평균·관리한계 이탈·런 규칙)는 {@link AiService#spcAnomaly} 결정론 로직으로 산출하고,
|
||||
* <b>원인 서술·분류·우선순위</b>만 중앙 {@code /rag/agent}(tool-use) + {@code /rag/structured}(JSON 강제)로 받는다.
|
||||
* 토글 off/미가용 시 기존 {@link AiService#defectRootCause}/{@code spcAnomaly} 로 무손실 폴백(degraded:true).</li>
|
||||
* <li><b>예측 분석(predictAnalysis)</b> — 설비 예지보전 + 수요/생산 예측.
|
||||
* 예측 수치(이동평균·추세·위험점수)는 {@link AiService#forecast}/{@code predictiveMaintenance} 결정론 베이스라인으로 산출하고,
|
||||
* <b>해석·권고 서술</b>만 {@code /rag/agent} 로 받는다. 토글 off/미가용 시 결정론 경로로 폴백(degraded:true).</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p><b>결정론 불변</b>: SPC/예측 수치는 절대 AI 가 만들지 않는다(재현 가능). AI 는 서술·분류·우선순위만.
|
||||
* <p>모든 응답에 적용 metadata(retrievalMode·rerank·toolUse·structured·degraded)를 표기해 토글 effect 가 관측 가능하게 한다.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RagMesService {
|
||||
|
||||
private final RagClient rag;
|
||||
private final RagToggleService toggles;
|
||||
private final AiService ai; // 기존 결정론/Ollama 폴백 재사용(불변)
|
||||
|
||||
/** 불량 RCA 결정론 결과 스키마(중앙 /structured/agent 강제). */
|
||||
private static final Map<String, Object> DEFECT_SCHEMA = Map.of(
|
||||
"type", "object",
|
||||
"properties", Map.of(
|
||||
"rootCauses", Map.of("type", "array", "items", Map.of(
|
||||
"type", "object", "properties", Map.of(
|
||||
"cause", Map.of("type", "string"),
|
||||
"evidenceRef", Map.of("type", "string"),
|
||||
"contribution", Map.of("type", "number")
|
||||
))),
|
||||
"correctiveActions", Map.of("type", "array", "items", Map.of("type", "string")),
|
||||
"priority", Map.of("type", "string"),
|
||||
"confidence", Map.of("type", "number")
|
||||
),
|
||||
"required", List.of("rootCauses")
|
||||
);
|
||||
|
||||
/** 예측 해석 결과 스키마(중앙 /agent 구조화). */
|
||||
private static final Map<String, Object> PREDICT_SCHEMA = Map.of(
|
||||
"type", "object",
|
||||
"properties", Map.of(
|
||||
"interpretation", Map.of("type", "string"),
|
||||
"drivers", Map.of("type", "array", "items", Map.of("type", "string")),
|
||||
"recommendation", Map.of("type", "string"),
|
||||
"confidence", Map.of("type", "number")
|
||||
),
|
||||
"required", List.of("interpretation")
|
||||
);
|
||||
|
||||
// 읽기전용 조회 도구 정의(이름·설명만 노출 — 실제 조회는 중앙에서 컬렉션 검색으로 수행. 쓰기/SSH 금지)
|
||||
private static final List<Map<String, Object>> DEFECT_TOOLS = List.of(
|
||||
tool("search_ncr", "부적합(NCR) 이력 검색 — 불량코드/LOT 기준 근거 수집(읽기전용)"),
|
||||
tool("search_capa", "시정조치(CAPA) 이력 검색 — 유사 불량의 과거 조치(읽기전용)"),
|
||||
tool("search_inspection", "검사 결과 검색 — 측정값·합부 판정 근거(읽기전용)")
|
||||
);
|
||||
private static final List<Map<String, Object>> PREDICT_TOOLS = List.of(
|
||||
tool("search_equipment", "설비 가동/비가동·MTBF 이력 검색(읽기전용)"),
|
||||
tool("search_production", "생산실적·작업지시 시계열 검색(읽기전용)")
|
||||
);
|
||||
|
||||
// ── 1) 품질 분석: 불량 RCA + SPC 이상감지 ──────────────────────────────────
|
||||
/**
|
||||
* 불량 원인분석 + SPC 이상감지. SPC 수치는 결정론, 원인 서술은 /rag/agent(+structured).
|
||||
* @param req {defectCode, context[], values[], ucl, lcl, cl}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> defectAnalysis(Map<String, Object> req, String actor) {
|
||||
String defectCode = str(req.get("defectCode"));
|
||||
List<Map<String, Object>> context = req.get("context") instanceof List
|
||||
? (List<Map<String, Object>>) req.get("context") : List.of();
|
||||
|
||||
// (A) SPC 수치 — 항상 결정론(재현 가능). AI 가 만들지 않는다.
|
||||
Map<String, Object> spc = null;
|
||||
if (req.get("values") instanceof List) {
|
||||
spc = ai.spcAnomaly(toDoubles(req.get("values")),
|
||||
dbl(req.get("ucl")), dbl(req.get("lcl")), dbl(req.get("cl")));
|
||||
}
|
||||
|
||||
RagToggles t = toggles.current();
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
if (spc != null) out.put("spc", spc); // 결정론 수치 결과
|
||||
|
||||
// (B) 원인 서술 — 토글 off/미가용 시 기존 결정론 RCA 폴백
|
||||
if (!t.ragEnabled || !rag.available()) {
|
||||
Map<String, Object> legacy = ai.defectRootCause(defectCode, context);
|
||||
out.put("defect", legacy);
|
||||
out.put("degraded", true);
|
||||
out.put("degraded_reason", t.ragEnabled ? "rag_unavailable" : "rag_disabled");
|
||||
out.put("source", "mes_local_fallback");
|
||||
out.putAll(meta(t, t.toolUse ? "agent" : "answer"));
|
||||
return out;
|
||||
}
|
||||
|
||||
String mode = toggles.effectiveMode(t);
|
||||
String task =
|
||||
"당신은 제조 품질 엔지니어입니다. 불량코드 '" + defectCode + "' 의 근본원인을 4M·5Why·파레토 관점으로 "
|
||||
+ "추정하고 시정조치를 제시하라. 제공된 NCR/CAPA/검사 근거에 기반해야 한다. "
|
||||
+ "수치 판정(SPC 관리한계·Cp/Cpk)은 이미 결정론으로 계산되었으니 재계산하지 말고 해석만 하라. "
|
||||
+ "근거: " + truncate(context.toString(), 1200)
|
||||
+ (spc != null ? "\nSPC결과: " + truncate(spc.toString(), 400) : "");
|
||||
|
||||
Map<String, Object> res;
|
||||
String technique;
|
||||
if (t.toolUse) {
|
||||
// 에이전틱 tool-use: 조회 매퍼 도구로 멀티스텝 + 구조화(structured on 시 스키마 결합)
|
||||
res = rag.agent(task, DEFECT_TOOLS, mode, t.maxSteps, t.generationModel,
|
||||
t.structured ? DEFECT_SCHEMA : null);
|
||||
technique = "agent";
|
||||
} else if (t.structured) {
|
||||
// tool-use off + structured on: 결정론 JSON
|
||||
res = rag.structured(task, DEFECT_SCHEMA, t.generationModel, t.temperature);
|
||||
technique = "structured";
|
||||
} else {
|
||||
// 둘 다 off: 검색 근거 답변
|
||||
res = rag.answer(task, mode, t.rerank, t.topK, t.generationModel, true);
|
||||
technique = "answer";
|
||||
}
|
||||
|
||||
boolean degraded = bool(res.get("degraded"));
|
||||
if (degraded) {
|
||||
Map<String, Object> legacy = ai.defectRootCause(defectCode, context);
|
||||
out.put("defect", legacy);
|
||||
out.put("degraded", true);
|
||||
out.put("degraded_reason", str(res.getOrDefault("degraded_reason", "rag_empty")));
|
||||
out.put("source", "mes_local_fallback");
|
||||
out.putAll(meta(t, technique));
|
||||
return out;
|
||||
}
|
||||
|
||||
// 정상 경유 — 구조화 데이터/근거/인용/answerId 표면화
|
||||
Map<String, Object> defect = new LinkedHashMap<>();
|
||||
if (res.get("data") instanceof Map) defect.putAll((Map<String, Object>) res.get("data"));
|
||||
if (res.get("answer") != null) defect.put("narrative", res.get("answer"));
|
||||
defect.put("citations", res.get("citations"));
|
||||
defect.put("sources", res.get("sources"));
|
||||
defect.put("steps", res.get("steps")); // 에이전트 추론 스텝(tool-use 시)
|
||||
defect.put("answerId", res.get("answer_id"));
|
||||
out.put("defect", defect);
|
||||
out.put("engine", "RAG_" + technique.toUpperCase());
|
||||
out.put("degraded", false);
|
||||
out.putAll(meta(t, technique));
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── 2) 예측 분석: 설비 예지보전 + 수요/생산 예측 ───────────────────────────
|
||||
/**
|
||||
* 설비 예지보전 + 수요/생산 예측. 예측 수치는 결정론 베이스라인, 해석/권고는 /rag/agent.
|
||||
* @param req {mode:"pdm"|"forecast", equipmentCode, availability, downtimeCount, mtbfHours, series[], horizon}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> predictAnalysis(Map<String, Object> req, String actor) {
|
||||
String kind = str(req.getOrDefault("kind", "forecast"));
|
||||
RagToggles t = toggles.current();
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
|
||||
// (A) 수치 베이스라인 — 항상 결정론(재현 가능). AI 가 만들지 않는다.
|
||||
Map<String, Object> baseline;
|
||||
if ("pdm".equalsIgnoreCase(kind)) {
|
||||
baseline = ai.predictiveMaintenance(str(req.get("equipmentCode")),
|
||||
dbl(req.get("availability")), (int) Math.round(dbl(req.get("downtimeCount"))),
|
||||
dbl(req.get("mtbfHours")));
|
||||
} else {
|
||||
baseline = ai.forecast(toDoubles(req.get("series")),
|
||||
(int) Math.round(dbl(req.get("horizon"))));
|
||||
}
|
||||
out.put("baseline", baseline);
|
||||
|
||||
// (B) 해석/권고 — 토글 off/미가용 시 베이스라인만(degraded)
|
||||
if (!t.ragEnabled || !rag.available()) {
|
||||
out.put("interpretation", null);
|
||||
out.put("degraded", true);
|
||||
out.put("degraded_reason", t.ragEnabled ? "rag_unavailable" : "rag_disabled");
|
||||
out.put("source", "mes_local_baseline");
|
||||
out.putAll(meta(t, t.toolUse ? "agent" : "answer"));
|
||||
return out;
|
||||
}
|
||||
|
||||
String mode = toggles.effectiveMode(t);
|
||||
String task =
|
||||
"당신은 제조 운영 분석가입니다. 아래 결정론 통계 결과를 해석하고 우선순위·권고를 제시하라. "
|
||||
+ "수치는 이미 계산되었으니 재계산하지 말고 의미·이상신호·조치만 서술하라. "
|
||||
+ ("pdm".equalsIgnoreCase(kind) ? "예지보전 결과" : "수요/생산 예측 결과") + ": "
|
||||
+ truncate(baseline.toString(), 800);
|
||||
|
||||
Map<String, Object> res;
|
||||
String technique;
|
||||
if (t.toolUse) {
|
||||
res = rag.agent(task, PREDICT_TOOLS, mode, t.maxSteps, t.generationModel,
|
||||
t.structured ? PREDICT_SCHEMA : null);
|
||||
technique = "agent";
|
||||
} else if (t.structured) {
|
||||
res = rag.structured(task, PREDICT_SCHEMA, t.generationModel, t.temperature);
|
||||
technique = "structured";
|
||||
} else {
|
||||
res = rag.answer(task, mode, t.rerank, t.topK, t.generationModel, true);
|
||||
technique = "answer";
|
||||
}
|
||||
|
||||
boolean degraded = bool(res.get("degraded"));
|
||||
if (degraded) {
|
||||
out.put("interpretation", null);
|
||||
out.put("degraded", true);
|
||||
out.put("degraded_reason", str(res.getOrDefault("degraded_reason", "rag_empty")));
|
||||
out.put("source", "mes_local_baseline");
|
||||
out.putAll(meta(t, technique));
|
||||
return out;
|
||||
}
|
||||
|
||||
Map<String, Object> interp = new LinkedHashMap<>();
|
||||
if (res.get("data") instanceof Map) interp.putAll((Map<String, Object>) res.get("data"));
|
||||
if (res.get("answer") != null) interp.put("narrative", res.get("answer"));
|
||||
interp.put("citations", res.get("citations"));
|
||||
interp.put("steps", res.get("steps"));
|
||||
interp.put("answerId", res.get("answer_id"));
|
||||
out.put("interpretation", interp);
|
||||
out.put("engine", "RAG_" + technique.toUpperCase());
|
||||
out.put("degraded", false);
|
||||
out.putAll(meta(t, technique));
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── 3) 피드백 → 중앙 /rag/feedback (solution=mes 격리) ─────────────────────
|
||||
public Map<String, Object> feedback(Map<String, Object> req, String actor) {
|
||||
String userRef = "u_" + Integer.toHexString((actor == null ? "anon" : actor).hashCode());
|
||||
return rag.feedback(
|
||||
str0(req.get("answerId")),
|
||||
str0(req.get("query")),
|
||||
str0(req.get("answer")),
|
||||
str(req.getOrDefault("verdict", "down")),
|
||||
str0(req.get("correction")),
|
||||
userRef);
|
||||
}
|
||||
|
||||
/** 현재 토글 스냅샷(화면용). */
|
||||
public Map<String, Object> currentToggles() {
|
||||
Map<String, Object> m = toggles.current().toMap();
|
||||
m.put("ragAvailable", rag.available());
|
||||
return m;
|
||||
}
|
||||
|
||||
// ── helpers ─────────────────────────────────────────────────────────────
|
||||
private static Map<String, Object> tool(String name, String desc) {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("name", name);
|
||||
m.put("description", desc);
|
||||
m.put("readonly", true);
|
||||
return m;
|
||||
}
|
||||
|
||||
private Map<String, Object> meta(RagToggles t, String technique) {
|
||||
Map<String, Object> applied = new LinkedHashMap<>();
|
||||
applied.put("retrievalMode", toggles.effectiveMode(t));
|
||||
applied.put("rerank", t.rerank);
|
||||
applied.put("graphrag", t.graphrag);
|
||||
applied.put("hybrid", t.hybrid);
|
||||
applied.put("toolUse", t.toolUse);
|
||||
applied.put("structured", t.structured);
|
||||
applied.put("stream", t.stream);
|
||||
applied.put("maxSteps", t.maxSteps);
|
||||
applied.put("technique", technique);
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("applied", applied);
|
||||
return m;
|
||||
}
|
||||
|
||||
private List<Double> toDoubles(Object o) {
|
||||
List<Double> out = new ArrayList<>();
|
||||
if (o instanceof List<?> list) {
|
||||
for (Object v : list) {
|
||||
try { out.add(Double.parseDouble(String.valueOf(v))); } catch (Exception ignore) {}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private double dbl(Object o) {
|
||||
try { return o == null ? 0.0 : Double.parseDouble(String.valueOf(o)); } catch (Exception e) { return 0.0; }
|
||||
}
|
||||
|
||||
private String truncate(String s, int max) {
|
||||
if (s == null) return "";
|
||||
return s.length() > max ? s.substring(0, max) : s;
|
||||
}
|
||||
|
||||
private boolean bool(Object o) { return Boolean.TRUE.equals(o) || "true".equalsIgnoreCase(String.valueOf(o)); }
|
||||
private String str(Object o) { return o == null ? "" : String.valueOf(o); }
|
||||
private String str0(Object o) { return o == null ? null : String.valueOf(o); }
|
||||
}
|
||||
41
backend/src/main/java/com/zioinfo/mes/rag/RagProperties.java
Normal file
41
backend/src/main/java/com/zioinfo/mes/rag/RagProperties.java
Normal file
@ -0,0 +1,41 @@
|
||||
package com.zioinfo.mes.rag;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 중앙 guardia-rag 연동 설정 (GUARDiA MES).
|
||||
*
|
||||
* <p><b>보안 불변</b>: base-url 은 <b>온프레미스(중앙 guardia-rag) 전용</b>이다.
|
||||
* 외부 엔드포인트를 가리키도록 설정해서는 안 된다. 기본값은 서버 내부 루프백이다.
|
||||
*
|
||||
* <p>application.yml:
|
||||
* <pre>
|
||||
* guardia:
|
||||
* rag:
|
||||
* base-url: http://127.0.0.1:8020
|
||||
* timeout-ms: 120000
|
||||
* enabled: true
|
||||
* solution: mes
|
||||
* </pre>
|
||||
*/
|
||||
@Component
|
||||
@Getter
|
||||
@Setter
|
||||
@ConfigurationProperties(prefix = "guardia.rag")
|
||||
public class RagProperties {
|
||||
|
||||
/** 중앙 guardia-rag 베이스 URL (서버 내부 루프백 기본). */
|
||||
private String baseUrl = "http://127.0.0.1:8020";
|
||||
|
||||
/** 호출 타임아웃(ms). 소형모델 생성 고려 120s 기본. */
|
||||
private long timeoutMs = 120_000L;
|
||||
|
||||
/** RAG 경유 마스터 스위치. false면 RagClient 가 항상 미가용으로 동작(MES 로컬 폴백). */
|
||||
private boolean enabled = true;
|
||||
|
||||
/** 솔루션 식별자(컬렉션·토글·피드백 격리 키). */
|
||||
private String solution = "mes";
|
||||
}
|
||||
139
backend/src/main/java/com/zioinfo/mes/rag/RagToggleService.java
Normal file
139
backend/src/main/java/com/zioinfo/mes/rag/RagToggleService.java
Normal file
@ -0,0 +1,139 @@
|
||||
package com.zioinfo.mes.rag;
|
||||
|
||||
import com.zioinfo.mes.admin.dto.MesSetting;
|
||||
import com.zioinfo.mes.admin.mapper.SettingMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MES RAG 기법 토글 서비스.
|
||||
*
|
||||
* <p>기존 {@code mes_setting}(key-value) 테이블을 재사용해 RAG 토글을 격리 저장한다(신규 설정 테이블
|
||||
* 신설 없음, admin 설정 화면과 자연 통합). 키 네임스페이스 {@code rag.*} 로 다른 설정과 충돌 회피.
|
||||
*
|
||||
* <p><b>서버 RAM 제약</b>: 무거운 기법(graphrag·rerank·tool_use·stream)은 <b>기본 off</b>.
|
||||
* retrieval_mode 기본 vector. step 상한·top_k 상한을 강제(폭주 차단).
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RagToggleService {
|
||||
|
||||
private final SettingMapper settingMapper;
|
||||
|
||||
public static final String K_ENABLED = "rag.enabled";
|
||||
public static final String K_MODE = "rag.retrieval_mode"; // vector|hybrid|graph
|
||||
public static final String K_RERANK = "rag.rerank"; // true|false (무거움→기본 off)
|
||||
public static final String K_GRAPHRAG = "rag.graphrag"; // true|false (무거움→기본 off)
|
||||
public static final String K_HYBRID = "rag.hybrid"; // true|false (mode 보조 스위치)
|
||||
public static final String K_TOOL_USE = "rag.tool_use"; // true|false (에이전틱 /agent, 무거움→off)
|
||||
public static final String K_STRUCTURED = "rag.structured"; // true|false (구조화 출력, 기본 on)
|
||||
public static final String K_STREAM = "rag.stream"; // true|false (SSE, 무거움→기본 off)
|
||||
public static final String K_TOP_K = "rag.top_k";
|
||||
public static final String K_MAX_STEPS = "rag.max_steps"; // 에이전트 step 상한
|
||||
public static final String K_TEMPERATURE = "rag.temperature";
|
||||
public static final String K_GEN_MODEL = "rag.generation_model";
|
||||
|
||||
/** 현재 토글 전부 조회(기본값 머지). 화면/배선 공용. */
|
||||
public RagToggles current() {
|
||||
Map<String, String> m = new LinkedHashMap<>();
|
||||
List<MesSetting> all = settingMapper.findAll();
|
||||
if (all != null) for (MesSetting s : all) m.put(s.getKey(), s.getValue());
|
||||
|
||||
RagToggles t = new RagToggles();
|
||||
t.ragEnabled = bool(m.get(K_ENABLED), true);
|
||||
t.retrievalMode = mode(m.get(K_MODE));
|
||||
t.rerank = bool(m.get(K_RERANK), false); // 무거움→off 기본
|
||||
t.graphrag = bool(m.get(K_GRAPHRAG), false); // 무거움→off 기본
|
||||
t.hybrid = bool(m.get(K_HYBRID), false); // 무거움→off 기본
|
||||
t.toolUse = bool(m.get(K_TOOL_USE), false); // 무거움→off 기본(에이전틱)
|
||||
t.structured = bool(m.get(K_STRUCTURED), true); // 결정론 출력→on 기본(경량)
|
||||
t.stream = bool(m.get(K_STREAM), false); // 무거움→off 기본
|
||||
t.topK = clampInt(m.get(K_TOP_K), 5, 1, 20);
|
||||
t.maxSteps = clampInt(m.get(K_MAX_STEPS), 4, 1, 8);
|
||||
t.temperature = clampDouble(m.get(K_TEMPERATURE), 0.1, 0.0, 1.0);
|
||||
t.generationModel = blankTo(m.get(K_GEN_MODEL), "llama3.2:1b");
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieval_mode 결정 우선순위: graphrag 토글 on → graph, 아니면 hybrid 토글 on → hybrid,
|
||||
* 아니면 설정된 mode(기본 vector).
|
||||
*/
|
||||
public String effectiveMode(RagToggles t) {
|
||||
if (t.graphrag) return "graph";
|
||||
if (t.hybrid) return "hybrid";
|
||||
return t.retrievalMode;
|
||||
}
|
||||
|
||||
// ── 정규화/클램프 (상한 강제 — 폭주·RAM 위협 차단) ────────────────────────
|
||||
private boolean bool(String v, boolean def) {
|
||||
if (v == null) return def;
|
||||
String s = v.trim();
|
||||
return "true".equalsIgnoreCase(s) || "1".equals(s) || "on".equalsIgnoreCase(s);
|
||||
}
|
||||
|
||||
private String mode(String v) {
|
||||
if (v == null) return "vector";
|
||||
String s = v.trim().toLowerCase();
|
||||
return (s.equals("hybrid") || s.equals("graph") || s.equals("vector")) ? s : "vector";
|
||||
}
|
||||
|
||||
private int clampInt(String v, int def, int min, int max) {
|
||||
try {
|
||||
int n = Integer.parseInt(v.trim());
|
||||
return Math.max(min, Math.min(max, n));
|
||||
} catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
private double clampDouble(String v, double def, double min, double max) {
|
||||
try {
|
||||
double n = Double.parseDouble(v.trim());
|
||||
return Math.max(min, Math.min(max, n));
|
||||
} catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
private String blankTo(String v, String def) {
|
||||
return (v == null || v.isBlank()) ? def : v.trim();
|
||||
}
|
||||
|
||||
/** 토글 스냅샷 DTO(응답 metadata·화면 공용). */
|
||||
public static class RagToggles {
|
||||
public boolean ragEnabled = true;
|
||||
public String retrievalMode = "vector";
|
||||
public boolean rerank = false;
|
||||
public boolean graphrag = false;
|
||||
public boolean hybrid = false;
|
||||
public boolean toolUse = false;
|
||||
public boolean structured = true;
|
||||
public boolean stream = false;
|
||||
public int topK = 5;
|
||||
public int maxSteps = 4;
|
||||
public double temperature = 0.1;
|
||||
public String generationModel = "llama3.2:1b";
|
||||
|
||||
public Map<String, Object> toMap() {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("ragEnabled", ragEnabled);
|
||||
m.put("retrievalMode", retrievalMode);
|
||||
m.put("rerank", rerank);
|
||||
m.put("graphrag", graphrag);
|
||||
m.put("hybrid", hybrid);
|
||||
m.put("toolUse", toolUse);
|
||||
m.put("structured", structured);
|
||||
m.put("stream", stream);
|
||||
m.put("topK", topK);
|
||||
m.put("maxSteps", maxSteps);
|
||||
m.put("temperature", temperature);
|
||||
m.put("generationModel", generationModel);
|
||||
return m;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -55,6 +55,13 @@ guardia:
|
||||
itsm-url: ${ITSM_URL:http://localhost:9001}
|
||||
ocr-url: ${OCR_URL:http://localhost:8005}
|
||||
bi-url: ${BI_URL:http://localhost:8006}
|
||||
# 중앙 guardia-rag (최신 AI 기법: 하이브리드/그래프/리랭크 검색·에이전틱 tool-use·구조화·스트리밍)
|
||||
# 보안 불변: 온프레미스 루프백 전용. 미가용/타임아웃 시 MES 결정론 로컬 폴백(degraded:true).
|
||||
rag:
|
||||
base-url: ${RAG_URL:http://127.0.0.1:8020}
|
||||
timeout-ms: ${RAG_TIMEOUT_MS:120000}
|
||||
enabled: ${RAG_ENABLED:true}
|
||||
solution: mes
|
||||
# 보안 불변 규칙: Ollama 온프레미스만 허용. 외부 AI API 절대 금지.
|
||||
ollama-url: ${OLLAMA_URL:http://localhost:11434}
|
||||
ollama-text-model: ${OLLAMA_TEXT_MODEL:llama3.2:1b}
|
||||
|
||||
@ -248,4 +248,24 @@ ALTER TABLE mes_user ADD COLUMN IF NOT EXISTS otp_secret VARCHAR(255);
|
||||
-- "업무 (UIWS)" 메뉴는 프론트 Sidebar/Route 에 추가(코드). 여기서는 주석으로만 명시.
|
||||
-- ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
-- ───────────────────────────────────────────────────────────────────────────
|
||||
-- [RAG 기법 토글] 중앙 guardia-rag 최신 AI 기법 적용 토글 (mes_setting 재사용).
|
||||
-- mes-ai-applier 배선용. 무거운 기법(graphrag·rerank·tool_use·stream)은 서버 RAM 제약상 기본 off.
|
||||
-- ON CONFLICT DO NOTHING 멱등 — 운영자가 화면에서 변경한 값은 보존(재시드 덮어쓰기 없음).
|
||||
-- ───────────────────────────────────────────────────────────────────────────
|
||||
INSERT INTO mes_setting (key, value) VALUES
|
||||
('rag.enabled', 'true'),
|
||||
('rag.retrieval_mode', 'vector'),
|
||||
('rag.rerank', 'false'),
|
||||
('rag.graphrag', 'false'),
|
||||
('rag.hybrid', 'false'),
|
||||
('rag.tool_use', 'false'),
|
||||
('rag.structured', 'true'),
|
||||
('rag.stream', 'false'),
|
||||
('rag.top_k', '5'),
|
||||
('rag.max_steps', '4'),
|
||||
('rag.temperature', '0.1'),
|
||||
('rag.generation_model', 'llama3.2:1b')
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
-- end 91_uiws_port.sql
|
||||
|
||||
@ -35,6 +35,7 @@ import Warehouses from './pages/Warehouses'
|
||||
// 공통
|
||||
import Analytics from './pages/Analytics'
|
||||
import AiTools from './pages/AiTools'
|
||||
import RagSettings from './pages/RagSettings'
|
||||
import UserManagement from './pages/UserManagement'
|
||||
import AuditLog from './pages/AuditLog'
|
||||
import SystemSettings from './pages/SystemSettings'
|
||||
@ -88,6 +89,9 @@ export default function App() {
|
||||
{/* 공통 */}
|
||||
<Route path="/analytics" element={<Analytics />} />
|
||||
<Route path="/ai-tools" element={<AiTools />} />
|
||||
<Route path="/ai-techniques" element={
|
||||
<ProtectedRoute min="MANAGER"><RagSettings /></ProtectedRoute>
|
||||
} />
|
||||
{/* 업무 (UIWS 이식) — 기존 라우트 보존, 추가만 */}
|
||||
<Route path="/uiws/worklog" element={<UiwsWorklog />} />
|
||||
<Route path="/uiws/schedule" element={<UiwsSchedule />} />
|
||||
|
||||
@ -300,6 +300,18 @@ export const aiInspectionJudge = (d: object) => unwrap(api.post('/api/mes/ai/ins
|
||||
export const aiSafetyStock = (d: object) => unwrap(api.post('/api/mes/ai/safety-stock', d))
|
||||
export const aiScheduleOptimize = (d: object) => unwrap(api.post('/api/mes/ai/schedule-optimize', d))
|
||||
|
||||
// ── RAG 최신 AI 기법 (중앙 guardia-rag 경유, 별개 레이어 — 항상 200, 폴백 degraded) ──
|
||||
// 불량 RCA + SPC 이상감지: SPC 수치는 결정론, 원인 서술은 /rag/agent(+structured)
|
||||
export const ragDefectAnalysis = (d: object) => unwrap(api.post('/api/mes/rag/defect-analysis', d))
|
||||
// 설비 예지보전 + 수요/생산 예측: 수치는 결정론 베이스라인, 해석은 /rag/agent
|
||||
export const ragPredictAnalysis = (d: object) => unwrap(api.post('/api/mes/rag/predict-analysis', d))
|
||||
// 👍/👎 피드백 (solution=mes 격리)
|
||||
export const ragFeedback = (d: object) => unwrap(api.post('/api/mes/rag/feedback', d))
|
||||
// 기법 토글 스냅샷 / 변경(MANAGER+)
|
||||
export const getRagToggles = () => unwrap(api.get('/api/mes/rag/toggles'))
|
||||
export const updateRagToggle = (key: string, value: any) =>
|
||||
unwrap(api.put(`/api/mes/rag/toggles/${key}`, { value }))
|
||||
|
||||
// ── Admin (SUPERADMIN / 감사로그·설정 GET 은 MANAGER+) ────────────────
|
||||
export const getUsers = () => api.get('/api/admin/users')
|
||||
export const createUser = (data: { username: string; password: string; displayName?: string; role?: string }) =>
|
||||
|
||||
@ -50,6 +50,7 @@ const masterLinks: Link[] = [
|
||||
const commonLinks: Link[] = [
|
||||
{ to: '/analytics', label: '분석 / KPI', icon: BarChart3 },
|
||||
{ to: '/ai-tools', label: 'AI 도구', icon: Sparkles },
|
||||
{ to: '/ai-techniques', label: 'AI 기법 설정', icon: Cpu, min: 'MANAGER' },
|
||||
]
|
||||
// 업무 (UIWS 이식) — 업무일지/일정/쪽지/업무통계
|
||||
const uiwsLinks: Link[] = [
|
||||
|
||||
@ -1,13 +1,54 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
Sparkles, AlertTriangle, TrendingUp, Wrench, LineChart, Search, Boxes, CalendarClock,
|
||||
Cpu, ThumbsUp, ThumbsDown,
|
||||
} from 'lucide-react'
|
||||
import { Card, Btn, Field } from '../components/ui'
|
||||
import {
|
||||
getAiStatus, aiDefectRootCause, aiForecast, aiPredictiveMaintenance,
|
||||
aiSpcAnomaly, aiParseQuery, aiSafetyStock, aiScheduleOptimize,
|
||||
ragDefectAnalysis, ragPredictAnalysis, ragFeedback,
|
||||
} from '../api/client'
|
||||
|
||||
/** 적용된 기법 배지 — 응답의 applied metadata 를 시각화(토글 effect 관측). */
|
||||
function AppliedBadge({ data }: { data: any }) {
|
||||
const a = data?.applied
|
||||
if (!a) return null
|
||||
const chips: string[] = [`mode:${a.retrievalMode}`, `기법:${a.technique}`]
|
||||
if (a.rerank) chips.push('rerank')
|
||||
if (a.graphrag) chips.push('graphrag')
|
||||
if (a.hybrid) chips.push('hybrid')
|
||||
if (a.toolUse) chips.push(`agent(${a.maxSteps})`)
|
||||
if (a.structured) chips.push('structured')
|
||||
if (a.stream) chips.push('stream')
|
||||
if (data?.degraded) chips.push('⚠ degraded(폴백)')
|
||||
return (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{chips.map((c, i) => (
|
||||
<span key={i} className={`text-[10px] px-1.5 py-0.5 rounded border ${c.startsWith('⚠') ? 'bg-amber-500/15 text-amber-300 border-amber-500/30' : 'bg-brand/10 text-brand border-brand/30'}`}>{c}</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 👍/👎 피드백 (중앙 /rag/feedback, solution=mes 격리). answerId 가 있을 때만 노출. */
|
||||
function FeedbackBar({ answerId, query }: { answerId?: string; query?: string }) {
|
||||
const [done, setDone] = useState('')
|
||||
if (!answerId) return null
|
||||
const send = async (verdict: 'up' | 'down') => {
|
||||
try { await ragFeedback({ answerId, query, verdict }); setDone(verdict === 'up' ? '👍 반영됨' : '👎 반영됨') } catch { setDone('전송 실패') }
|
||||
}
|
||||
return (
|
||||
<div className="mt-2 flex items-center gap-2 text-xs text-slate-400">
|
||||
<span>이 답변이 도움이 됐나요?</span>
|
||||
<button onClick={() => send('up')} className="p-1 rounded hover:bg-brand/10 hover:text-brand"><ThumbsUp size={13} /></button>
|
||||
<button onClick={() => send('down')} className="p-1 rounded hover:bg-rose-500/10 hover:text-rose-300"><ThumbsDown size={13} /></button>
|
||||
{done && <span className="text-brand">{done}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Output({ data }: { data: any }) {
|
||||
if (data == null) return null
|
||||
if (typeof data === 'string') return <pre className="mt-3 text-xs text-slate-300 bg-ink border border-edge rounded p-3 whitespace-pre-wrap max-h-56 overflow-auto">{data}</pre>
|
||||
@ -31,6 +72,17 @@ export default function AiTools() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* ── 최신 기법 (중앙 guardia-rag 경유) — 대표 2개 기능 ───────────── */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-semibold flex items-center gap-2 text-brand"><Cpu size={15} /> 최신 AI 기법 (RAG · 에이전틱 · 구조화)</h2>
|
||||
<Link to="/ai-techniques" className="text-xs text-slate-400 hover:text-brand underline">기법 토글 설정 →</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5 mb-8">
|
||||
<RagDefectTool /><RagPredictTool />
|
||||
</div>
|
||||
|
||||
{/* ── 기존 AI 도구 (불변, Ollama + Java 폴백) ───────────────────── */}
|
||||
<h2 className="text-sm font-semibold text-slate-300 mb-3">기본 AI 도구</h2>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
<DefectTool /><ForecastTool /><MaintenanceTool /><SpcTool />
|
||||
<QueryTool /><SafetyStockTool /><ScheduleTool />
|
||||
@ -39,6 +91,85 @@ export default function AiTools() {
|
||||
)
|
||||
}
|
||||
|
||||
/** 대표 1: 불량 원인분석 + SPC 이상감지 (SPC 수치=결정론, 원인서술=/rag/agent+structured). */
|
||||
function RagDefectTool() {
|
||||
const [code, setCode] = useState(''); const [ctx, setCtx] = useState('')
|
||||
const [values, setValues] = useState(''); const [ucl, setUcl] = useState(''); const [lcl, setLcl] = useState(''); const [cl, setCl] = useState('')
|
||||
const [out, setOut] = useState<any>(null); const [busy, setBusy] = useState(false)
|
||||
const run = async () => {
|
||||
setBusy(true)
|
||||
try {
|
||||
const vals = values.split(',').map(s => s.trim()).filter(Boolean).map(Number)
|
||||
const req: any = { defectCode: code, context: ctx ? [{ note: ctx }] : [] }
|
||||
if (vals.length) { req.values = vals; req.ucl = Number(ucl) || 0; req.lcl = Number(lcl) || 0; req.cl = Number(cl) || 0 }
|
||||
setOut(await ragDefectAnalysis(req))
|
||||
} finally { setBusy(false) }
|
||||
}
|
||||
return (
|
||||
<Card title="불량 원인분석 + SPC 이상감지 (RAG)" icon={<AlertTriangle size={15} />}>
|
||||
<Field label="불량 코드"><input className="inp" value={code} onChange={e => setCode(e.target.value)} placeholder="예: D-DIM-01" /></Field>
|
||||
<Field label="컨텍스트(공정/설비/자재)"><textarea className="inp" value={ctx} onChange={e => setCtx(e.target.value)} /></Field>
|
||||
<Field label="SPC 표본값(쉼표구분, 선택)"><input className="inp" value={values} onChange={e => setValues(e.target.value)} placeholder="예: 10.1, 10.3, 9.8, 10.5" /></Field>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<Field label="UCL"><input className="inp" value={ucl} onChange={e => setUcl(e.target.value)} /></Field>
|
||||
<Field label="LCL"><input className="inp" value={lcl} onChange={e => setLcl(e.target.value)} /></Field>
|
||||
<Field label="CL"><input className="inp" value={cl} onChange={e => setCl(e.target.value)} /></Field>
|
||||
</div>
|
||||
<Btn onClick={run} disabled={busy} size="sm"><Cpu size={13} /> {busy ? '분석 중…' : 'RCA + SPC 분석'}</Btn>
|
||||
<AppliedBadge data={out} />
|
||||
<Output data={out} />
|
||||
<FeedbackBar answerId={out?.defect?.answerId} query={code} />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
/** 대표 2: 설비 예지보전 / 수요·생산 예측 (수치=결정론 베이스라인, 해석=/rag/agent). */
|
||||
function RagPredictTool() {
|
||||
const [kind, setKind] = useState<'pdm' | 'forecast'>('forecast')
|
||||
const [eq, setEq] = useState(''); const [avail, setAvail] = useState(''); const [dt, setDt] = useState(''); const [mtbf, setMtbf] = useState('')
|
||||
const [series, setSeries] = useState(''); const [horizon, setHorizon] = useState(7)
|
||||
const [out, setOut] = useState<any>(null); const [busy, setBusy] = useState(false)
|
||||
const run = async () => {
|
||||
setBusy(true)
|
||||
try {
|
||||
const req: any = { kind }
|
||||
if (kind === 'pdm') { req.equipmentCode = eq; req.availability = Number(avail) || 0; req.downtimeCount = Number(dt) || 0; req.mtbfHours = Number(mtbf) || 0 }
|
||||
else { req.series = series.split(',').map(s => s.trim()).filter(Boolean).map(Number); req.horizon = horizon }
|
||||
setOut(await ragPredictAnalysis(req))
|
||||
} finally { setBusy(false) }
|
||||
}
|
||||
return (
|
||||
<Card title="예지보전 / 수요·생산 예측 (RAG)" icon={<TrendingUp size={15} />}>
|
||||
<div className="flex gap-2 mb-2">
|
||||
{(['forecast', 'pdm'] as const).map(k => (
|
||||
<button key={k} onClick={() => setKind(k)} className={`px-3 py-1.5 text-xs rounded-lg border ${kind === k ? 'bg-brand text-ink border-brand' : 'bg-card border-edge text-slate-300'}`}>
|
||||
{k === 'forecast' ? '수요/생산 예측' : '설비 예지보전'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{kind === 'pdm' ? (
|
||||
<>
|
||||
<Field label="설비 코드"><input className="inp" value={eq} onChange={e => setEq(e.target.value)} placeholder="예: EQ-CNC-01" /></Field>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<Field label="가동률(0~1)"><input className="inp" value={avail} onChange={e => setAvail(e.target.value)} /></Field>
|
||||
<Field label="비가동 횟수"><input className="inp" value={dt} onChange={e => setDt(e.target.value)} /></Field>
|
||||
<Field label="MTBF(시간)"><input className="inp" value={mtbf} onChange={e => setMtbf(e.target.value)} /></Field>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Field label="시계열(쉼표구분)"><input className="inp" value={series} onChange={e => setSeries(e.target.value)} placeholder="예: 100, 120, 95, 130" /></Field>
|
||||
<Field label="예측 기간(일)"><input type="number" className="inp" value={horizon} onChange={e => setHorizon(Number(e.target.value))} /></Field>
|
||||
</>
|
||||
)}
|
||||
<Btn onClick={run} disabled={busy} size="sm"><Cpu size={13} /> {busy ? '분석 중…' : '예측 + 해석'}</Btn>
|
||||
<AppliedBadge data={out} />
|
||||
<Output data={out} />
|
||||
<FeedbackBar answerId={out?.interpretation?.answerId} query={kind} />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function DefectTool() {
|
||||
const [code, setCode] = useState(''); const [ctx, setCtx] = useState(''); const [out, setOut] = useState<any>(null); const [busy, setBusy] = useState(false)
|
||||
const run = async () => { setBusy(true); try { setOut(await aiDefectRootCause({ defectCode: code, context: ctx })) } finally { setBusy(false) } }
|
||||
|
||||
153
frontend/src/pages/RagSettings.tsx
Normal file
153
frontend/src/pages/RagSettings.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Cpu, Save, Info, ShieldCheck } from 'lucide-react'
|
||||
import { Card, Btn } from '../components/ui'
|
||||
import { getRagToggles, updateRagToggle } from '../api/client'
|
||||
import { canManage } from '../components/rbac'
|
||||
|
||||
/**
|
||||
* MES AI 기법 설정 화면 — 중앙 guardia-rag 최신 기법 토글(솔루션별 격리, mes_setting 저장).
|
||||
*
|
||||
* - rerank·graphrag·hybrid·tool_use·structured·stream on/off + 파라미터(top_k·max_steps·temperature·model).
|
||||
* - 무거운 기법(graphrag·rerank·tool_use·stream)은 서버 RAM 제약상 기본 off — 경고 배지 표시.
|
||||
* - 변경은 MANAGER+ 권한(SecurityConfig PUT /api/mes/rag/toggles/** = MANAGER+). 그 외 조회 전용.
|
||||
*/
|
||||
|
||||
type Toggles = {
|
||||
ragEnabled: boolean; ragAvailable: boolean
|
||||
retrievalMode: string
|
||||
rerank: boolean; graphrag: boolean; hybrid: boolean
|
||||
toolUse: boolean; structured: boolean; stream: boolean
|
||||
topK: number; maxSteps: number; temperature: number; generationModel: string
|
||||
}
|
||||
|
||||
const HEAVY = new Set(['rag.graphrag', 'rag.rerank', 'rag.tool_use', 'rag.stream'])
|
||||
|
||||
export default function RagSettings() {
|
||||
const [t, setT] = useState<Toggles | null>(null)
|
||||
const [msg, setMsg] = useState(''); const [err, setErr] = useState('')
|
||||
const editable = canManage()
|
||||
|
||||
const load = () =>
|
||||
getRagToggles()
|
||||
.then((d: any) => { setT(d); setErr('') })
|
||||
.catch(() => setErr('RAG 토글을 불러오지 못했습니다.'))
|
||||
useEffect(() => { load() }, [])
|
||||
|
||||
const save = async (key: string, value: any) => {
|
||||
setMsg(''); setErr('')
|
||||
try { const d = await updateRagToggle(key, value); setT(d); setMsg(`'${key}' 저장됨`) }
|
||||
catch (e: any) { setErr(e?.response?.status === 403 ? '변경은 Manager+ 권한이 필요합니다.' : '저장 실패') }
|
||||
}
|
||||
|
||||
if (!t) return <div className="text-slate-400 text-sm">{err || '불러오는 중…'}</div>
|
||||
|
||||
const Toggle = ({ k, label, desc, on }: { k: string; label: string; desc: string; on: boolean }) => (
|
||||
<div className="flex items-start justify-between gap-3 py-2.5 border-b border-edge last:border-0">
|
||||
<div>
|
||||
<div className="text-sm font-medium flex items-center gap-2">
|
||||
{label}
|
||||
{HEAVY.has(k) && <span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-500/15 text-amber-300 border border-amber-500/30">무거움</span>}
|
||||
</div>
|
||||
<div className="text-xs text-slate-400 mt-0.5">{desc}</div>
|
||||
</div>
|
||||
<button
|
||||
disabled={!editable}
|
||||
onClick={() => save(k, !on)}
|
||||
className={`shrink-0 w-11 h-6 rounded-full transition-colors relative disabled:opacity-40 ${on ? 'bg-brand' : 'bg-slate-600'}`}
|
||||
title={on ? 'ON' : 'OFF'}
|
||||
>
|
||||
<span className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all ${on ? 'left-5' : 'left-0.5'}`} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold flex items-center gap-2"><Cpu className="text-brand" size={20} /> AI 기법 설정</h1>
|
||||
<p className="text-sm text-slate-400 mt-0.5">중앙 guardia-rag 최신 기법 적용(검색 모드·리랭크·GraphRAG·에이전틱·구조화·스트리밍). 솔루션별 격리 저장.</p>
|
||||
</div>
|
||||
<span className={`flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-lg border ${t.ragAvailable ? 'text-brand border-brand/30 bg-brand/10' : 'text-slate-400 border-edge bg-card'}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${t.ragAvailable ? 'bg-brand' : 'bg-slate-500'}`} />
|
||||
중앙 RAG {t.ragAvailable ? '연결됨' : '미가용 (로컬 폴백)'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{msg && <div className="bg-brand/10 border border-brand/30 text-brand text-sm rounded-lg px-4 py-2.5 mb-4">{msg}</div>}
|
||||
{err && <div className="bg-rose-500/10 border border-rose-500/40 text-rose-300 text-sm rounded-lg px-4 py-2.5 mb-4">{err}</div>}
|
||||
{!editable && <div className="bg-amber-500/10 border border-amber-500/30 text-amber-300 text-sm rounded-lg px-4 py-2.5 mb-4">조회 전용입니다. 기법 변경은 Manager+ 권한이 필요합니다.</div>}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
<Card title="마스터 / 검색 모드" icon={<ShieldCheck size={15} />}>
|
||||
<Toggle k="rag.enabled" label="중앙 RAG 경유" desc="off 시 MES 결정론 로컬 폴백으로 즉시 회귀(기존 동작)." on={t.ragEnabled} />
|
||||
<div className="py-2.5 border-b border-edge">
|
||||
<div className="text-sm font-medium mb-1.5">검색 모드 (retrieval_mode)</div>
|
||||
<div className="flex gap-2">
|
||||
{['vector', 'hybrid', 'graph'].map(m => (
|
||||
<button key={m} disabled={!editable} onClick={() => save('rag.retrieval_mode', m)}
|
||||
className={`px-3 py-1.5 text-xs rounded-lg border disabled:opacity-40 ${t.retrievalMode === m ? 'bg-brand text-ink border-brand' : 'bg-card border-edge text-slate-300 hover:border-brand'}`}>
|
||||
{m}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-xs text-slate-400 mt-1.5">graphrag/hybrid 토글이 켜지면 그 모드가 우선 적용됩니다.</div>
|
||||
</div>
|
||||
<Toggle k="rag.hybrid" label="하이브리드 검색" desc="BM25 키워드 + 벡터 앙상블. 검색 정확도↑(중간 비용)." on={t.hybrid} />
|
||||
<Toggle k="rag.rerank" label="리랭킹 (rerank)" desc="cross-encoder/LLM 재정렬. 상위 정밀도↑(추가 추론 비용)." on={t.rerank} />
|
||||
<Toggle k="rag.graphrag" label="GraphRAG" desc="지식그래프 기반 검색. 다중 홉 질의에 유리(고비용)." on={t.graphrag} />
|
||||
</Card>
|
||||
|
||||
<Card title="에이전틱 / 출력" icon={<Info size={15} />}>
|
||||
<Toggle k="rag.tool_use" label="에이전틱 tool-use (/agent)" desc="조회 매퍼를 도구로 멀티스텝 추론. 불량 RCA·예측 해석에 활용(고비용)." on={t.toolUse} />
|
||||
<Toggle k="rag.structured" label="구조화 출력 (/structured)" desc="JSON 스키마 강제 — 결정론적 결과 객체(경량, 기본 ON 권장)." on={t.structured} />
|
||||
<Toggle k="rag.stream" label="토큰 스트리밍 (SSE)" desc="긴 답변 점진 표시. 동시성 위협 시 off 유지(고비용)." on={t.stream} />
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 mt-3">
|
||||
<NumField label="top_k (1~20)" value={t.topK} min={1} max={20} disabled={!editable} onSave={v => save('rag.top_k', v)} />
|
||||
<NumField label="max_steps (1~8)" value={t.maxSteps} min={1} max={8} disabled={!editable} onSave={v => save('rag.max_steps', v)} />
|
||||
<NumField label="temperature (0~1)" value={t.temperature} min={0} max={1} step={0.1} disabled={!editable} onSave={v => save('rag.temperature', v)} />
|
||||
<TextField label="생성 모델" value={t.generationModel} disabled={!editable} onSave={v => save('rag.generation_model', v)} />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-xs text-slate-500">
|
||||
보안 불변: 외부 API 0 (중앙·로컬 모두 Ollama 온프레미스). 무거운 기법은 서버 RAM 제약상 기본 off — 미가용 시 결정론 폴백(degraded:true).
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NumField({ label, value, min, max, step = 1, disabled, onSave }:
|
||||
{ label: string; value: number; min: number; max: number; step?: number; disabled?: boolean; onSave: (v: number) => void }) {
|
||||
const [v, setV] = useState(String(value))
|
||||
useEffect(() => { setV(String(value)) }, [value])
|
||||
return (
|
||||
<div>
|
||||
<label className="text-xs text-slate-400">{label}</label>
|
||||
<div className="flex gap-1.5 mt-1">
|
||||
<input type="number" min={min} max={max} step={step} value={v} disabled={disabled}
|
||||
onChange={e => setV(e.target.value)}
|
||||
className="w-full bg-ink border border-edge rounded px-2 py-1 text-sm disabled:opacity-50" />
|
||||
<Btn size="sm" disabled={disabled} onClick={() => onSave(Number(v))}><Save size={12} /></Btn>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TextField({ label, value, disabled, onSave }:
|
||||
{ label: string; value: string; disabled?: boolean; onSave: (v: string) => void }) {
|
||||
const [v, setV] = useState(value)
|
||||
useEffect(() => { setV(value) }, [value])
|
||||
return (
|
||||
<div>
|
||||
<label className="text-xs text-slate-400">{label}</label>
|
||||
<div className="flex gap-1.5 mt-1">
|
||||
<input value={v} disabled={disabled} onChange={e => setV(e.target.value)}
|
||||
className="w-full bg-ink border border-edge rounded px-2 py-1 text-sm disabled:opacity-50" />
|
||||
<Btn size="sm" disabled={disabled} onClick={() => onSave(v)}><Save size={12} /></Btn>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
140
scripts/rag_ingest_mes.py
Normal file
140
scripts/rag_ingest_mes.py
Normal file
@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GUARDiA MES — 중앙 guardia-rag 색인 훅 (대표 도메인 메타 → mes 컬렉션).
|
||||
|
||||
불량 RCA·SPC 해석·예지보전/예측 해석의 근거 검색을 위해, MES 의 대표 도메인
|
||||
**메타데이터(테이블/컬럼/판정규칙/방법 정의)** 만 중앙 RAG 의 `mes` 컬렉션에 적재한다.
|
||||
실제 생산/검사 데이터 행·거래처/인사 PII·자격증명은 색인하지 않는다(보안 불변).
|
||||
|
||||
사용:
|
||||
python scripts/rag_ingest_mes.py # 기본(http://127.0.0.1:8020)
|
||||
RAG_URL=http://127.0.0.1:8020 python scripts/rag_ingest_mes.py
|
||||
|
||||
설계 메모:
|
||||
- 외부 API 0. 중앙 guardia-rag(온프레미스)만 호출.
|
||||
- 멱등: doc_id 안정 → 재실행 시 upsert(중앙이 처리). 실패해도 MES 동작에 영향 없음(보조 스크립트).
|
||||
- PII/자격증명 미색인: 거래처 연락처·담당자명·SSH·비밀번호·IP 는 문서에 포함하지 않는다.
|
||||
- 수치 결정론 불변: SPC 관리한계·Cp/Cpk·예측치는 MES Java 로직이 산출. 본 문서는 "방법·판정규칙 정의"만
|
||||
제공해 AI 가 수치를 재계산하지 않고 해석만 하도록 grounding 한다.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
RAG_URL = os.environ.get("RAG_URL", "http://127.0.0.1:8020").rstrip("/")
|
||||
SOLUTION = "mes"
|
||||
|
||||
# 대표 도메인 메타 문서 — 불량 RCA·SPC·예측 해석 근거용.
|
||||
# (PII/자격증명 컬럼 제외. 판정규칙·방법론·논리지표↔물리컬럼 중심.)
|
||||
DOCS = [
|
||||
{
|
||||
"doc_id": "mes_qms_ncr",
|
||||
"source": "MES-SCHEMA-QMS",
|
||||
"text": (
|
||||
"부적합(NCR) 도메인: 검사/공정에서 발견된 부적합 기록. 핵심 필드 - ncr_no(부적합번호), "
|
||||
"defect_code(불량코드), lot_no(LOT), item_code(품목), process_code(공정), "
|
||||
"equipment_code(설비), qty_defect(불량수량), root_cause(추정원인), status. "
|
||||
"불량코드 접두 규칙: DIM=치수, SCR/외관=표면손상, MAT=자재편차. "
|
||||
"원인분석은 4M(Man/Machine/Material/Method)·5Why·파레토 관점으로 수행한다."
|
||||
),
|
||||
},
|
||||
{
|
||||
"doc_id": "mes_qms_capa",
|
||||
"source": "MES-SCHEMA-QMS",
|
||||
"text": (
|
||||
"시정조치(CAPA) 도메인: 부적합에 대한 시정·예방 조치. 필드 - capa_no, ncr_no(연결 부적합), "
|
||||
"action_type(CORRECTIVE/PREVENTIVE), action_desc(조치내용), owner_role, due_date, status, "
|
||||
"effectiveness(유효성 검증결과). 유사 불량의 과거 CAPA 이력은 RCA 근거로 활용한다."
|
||||
),
|
||||
},
|
||||
{
|
||||
"doc_id": "mes_qms_inspection",
|
||||
"source": "MES-SCHEMA-QMS",
|
||||
"text": (
|
||||
"검사(inspection) 도메인: 수입/공정/출하 검사 결과. 필드 - inspection_no, item_code, lot_no, "
|
||||
"characteristic(검사항목), measured(실측값), lsl(하한규격), usl(상한규격), target(목표), "
|
||||
"judgement(PASS/FAIL/REINSPECT), aql. 판정규칙: measured<lsl 또는 measured>usl 이면 FAIL. "
|
||||
"deviation = measured - target. 최종 판정 권한은 사람에게 있으며 AI 는 초안만 제시한다."
|
||||
),
|
||||
},
|
||||
{
|
||||
"doc_id": "mes_method_spc",
|
||||
"source": "MES-METHOD-SPC",
|
||||
"text": (
|
||||
"SPC(통계적공정관리) 방법 정의 — 관리도 X̄-R, p, np, c, u. 런 규칙(Western Electric/Nelson): "
|
||||
"RULE1 관리한계 이탈(>UCL 또는 <LCL), RULE2 연속 9점이 중심선 한쪽, RULE3 연속 6점 증가/감소 추세. "
|
||||
"공정능력: Cp=(USL-LSL)/(6σ), Cpk=min((USL-μ),(μ-LSL))/(3σ). "
|
||||
"★수치(평균·UCL·LCL·Cp/Cpk·이탈점)는 MES 결정론 로직이 계산한다. AI 는 이탈 의미·원인·조치만 해석한다(재계산 금지)."
|
||||
),
|
||||
},
|
||||
{
|
||||
"doc_id": "mes_equipment_oee",
|
||||
"source": "MES-SCHEMA-MES",
|
||||
"text": (
|
||||
"설비/OEE 도메인: 설비 가동·비가동·종합효율. 필드 - equipment_code, availability(가동률 0~1), "
|
||||
"performance, quality, oee(=가용성*성능*품질), downtime_count(비가동 횟수), mtbf_hours(평균고장간격), "
|
||||
"mttr_hours(평균수리시간). 예지보전 위험 규칙: 가동률 낮고(<0.7) 비가동 잦으며 MTBF 짧을수록(<100h) 고위험. "
|
||||
"★위험점수·정비시점은 MES 결정론 로직이 산출하고 AI 는 추세 해석·우선순위만 제시한다."
|
||||
),
|
||||
},
|
||||
{
|
||||
"doc_id": "mes_production_forecast",
|
||||
"source": "MES-METHOD-FORECAST",
|
||||
"text": (
|
||||
"수요/생산 예측 방법 정의 — 생산실적(job)·작업지시·출하 시계열 기반. 베이스라인: 가중 이동평균 + 선형추세 "
|
||||
"(예측 = (마지막값 + 기울기*i)*0.6 + 평균*0.4, 음수 클램프). horizon 기본 7일, 최대 90일. "
|
||||
"★예측 수치는 MES 결정론 로직이 산출(재현 가능). AI 는 추세 해석·이상신호 강조·생산계획 조정 권고만 한다."
|
||||
),
|
||||
},
|
||||
{
|
||||
"doc_id": "mes_inventory_optimize",
|
||||
"source": "MES-METHOD-INVENTORY",
|
||||
"text": (
|
||||
"재고 최적화 방법 정의 — 안전재고 = Z * 수요표준편차 * sqrt(리드타임), 발주점(ROP) = 평균일수요*리드타임 + 안전재고. "
|
||||
"기본 서비스수준 95%(Z=1.65). LOT 만료 위험은 lotserial 의 expiry_date 로 감지. "
|
||||
"★수치는 MES 결정론 로직이 산출. AI 는 과부족·만료 위험 해석과 발주 권고만 제시한다."
|
||||
),
|
||||
},
|
||||
{
|
||||
"doc_id": "mes_rag_contract",
|
||||
"source": "MES-RAG",
|
||||
"text": (
|
||||
"MES 최신 AI 기법 적용(중앙 guardia-rag): 대표 2기능 — (1) 불량 RCA+SPC 이상감지: SPC 수치는 결정론, "
|
||||
"원인서술은 /rag/agent(tool-use)+/rag/structured(JSON 강제). (2) 예지보전/수요·생산 예측: 수치는 결정론 베이스라인, "
|
||||
"해석은 /rag/agent. 기법 토글(rerank·graphrag·hybrid·tool_use·structured·stream)은 mes_setting 의 rag.* 키로 격리 저장. "
|
||||
"무거운 기법은 서버 RAM 제약상 기본 off, 미가용 시 결정론 폴백(degraded:true). 외부 API 0(Ollama 온프레미스)."
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def ingest():
|
||||
payload = {"solution": SOLUTION, "documents": DOCS}
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
f"{RAG_URL}/rag/ingest",
|
||||
data=data,
|
||||
headers={"Content-Type": "application/json", "X-Solution-Key": SOLUTION},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=120) as resp:
|
||||
body = resp.read().decode("utf-8")
|
||||
print(f"[OK] ingested {len(DOCS)} docs into '{SOLUTION}' collection")
|
||||
print(f" response: {body[:300]}")
|
||||
return 0
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"[WARN] ingest HTTP {e.code} — 중앙 RAG 응답 이상(스크립트는 보조). 본 MES 동작 영향 없음.")
|
||||
return 1
|
||||
except urllib.error.URLError as e:
|
||||
print(f"[WARN] 중앙 guardia-rag 미가용({RAG_URL}) — 색인 보류. 라이브 가동 후 재실행. (사유 요약: {str(e.reason)[:80]})")
|
||||
return 1
|
||||
except Exception:
|
||||
print("[WARN] 색인 중 예기치 못한 오류 — 보류(MES 동작 영향 없음).")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(ingest())
|
||||
Loading…
Reference in New Issue
Block a user