feat: 운전판정 고도화 — realtime stall 수정 + 교차검증 + 단위/레인지

- ExperionRealtimeService를 단일 SuperviseAsync supervisor로 재설계:
  비블로킹 부팅, PublishingStopped/KeepAliveStopped 워치독으로 silent
  stall 감지, 30초 주기 무한 재연결, flush 루프 단일화
- RealtimeServiceStatus에 LastDataAgeSeconds/Stalled 추가, History는
  Stalled 시 스냅샷 skip
- v_plant_running_state에 진공펌프(vp-) 포함 + 교차검증 4객체
  (pump_corroboration_manual, v_pump_signal_map,
  v_plant_running_state_corroborated, v_plant_running_state_agg)
  + v_instrument_range 뷰 (boot DDL)
- MetadataLoaderService에 euhi/eulo/units 메타속성 추가
- generate_status_report에 agg 조회 연동 + sample/focus 버그 수정
- plant_context.md에 펌프 prefix(p-/vp-) + 교차검증 뷰 사용법

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
windpacer
2026-05-24 16:47:20 +09:00
parent 7dbeb36218
commit 2e844abf11
9 changed files with 1015 additions and 183 deletions

View File

@@ -1708,6 +1708,25 @@ async def generate_status_report(area: str | None = None, hours: int = 24) -> st
for ev in events:
by_type[ev["event_type"]] = by_type.get(ev["event_type"], 0) + 1
# 2.5) 펌프 운전 교차검증 (유량/진공 기반) — v_plant_running_state_agg
pump_corr: list[dict] = []
try:
corr_raw = await _execute_sql_internal(
"SELECT area_code, status, total_pumps, confirmed_running, suspicious_running, "
"stale_running, indeterminate_running, tripped_pumps, "
"confirmed_tags, suspicious_tags, stale_tags "
"FROM v_plant_running_state_agg ORDER BY area_code"
)
corr_parsed = json.loads(corr_raw)
if corr_parsed.get("success"):
pump_corr = corr_parsed.get("data", [])
if area:
_f = [r for r in pump_corr if (r.get("area_code") or "").upper() == area.upper()]
if _f:
pump_corr = _f
except Exception:
pump_corr = []
# 3) LLM 보고서
alarm_lines = [
f"- [{a['event_type']}] {a['tag_name']} since {_kst_str(a['since'])} "
@@ -1718,9 +1737,16 @@ async def generate_status_report(area: str | None = None, hours: int = 24) -> st
f"- [{ev['event_type']}] {ev['tag_name']} @ {_kst_str(ev['event_time'])} "
f"({ev.get('prev_value')}{ev.get('curr_value')})"
f" (직전상태유지={ev.get('prev_state_duration_s', '?')}s)"
for ev in sample
for ev in events[:40]
]
focus_line = f"\n특히 다음 관점을 우선해 설명하세요: {focus}\n" if focus else ""
corr_lines = [
f"- {r.get('area_code')}: {r.get('status')} "
f"(확인 {r.get('confirmed_running', 0)}, 의심 {r.get('suspicious_running', 0)}, "
f"정체 {r.get('stale_running', 0)}, 트립 {r.get('tripped_pumps', 0)})"
+ (f" 의심펌프={r.get('suspicious_tags')}" if r.get('suspicious_running') else "")
+ (f" 정체펌프={r.get('stale_tags')}" if r.get('stale_running') else "")
for r in pump_corr
] or ["- 펌프 교차검증 데이터 없음"]
system = (
"당신은 IIoT/공장 운전 분석 전문가입니다. 디지털 포인트의 상태 변경 이벤트 로그를 보고 "
@@ -1731,13 +1757,17 @@ async def generate_status_report(area: str | None = None, hours: int = 24) -> st
"4) 다음 점검 권고 (있다면)\n"
"구체적인 태그명과 시각을 포함하되 추측은 자제합니다.\n\n"
"참고 - 모든 시각은 KST(UTC+9, Asia/Seoul)입니다. "
"`직전상태유지`는 이 이벤트 직전에 태그가 머물렀던 시간(초)입니다."
"`직전상태유지`는 이 이벤트 직전에 태그가 머물렀던 시간(초)입니다.\n"
"펌프 운전 교차검증: 펌프 RUN 상태를 유량(kg/hr)·진공압(torr)으로 확인한 결과입니다. "
"'확인'=실질 운전, '의심'=RUN인데 유량/진공 없음(deadhead·센서이상 가능), "
"'정체'=실시간 수집 지연으로 판정 보류. 의심/정체가 있으면 보고서에 우선 명시하세요."
)
user_msg = (
f"대상 area: {area or '전체'}\n"
f"분석 윈도우: 최근 {hours}시간\n"
f"이벤트 통계 (type별): {by_type}\n"
f"활성 알람 {len(alarms)}건:\n" + "\n".join(alarm_lines) + "\n\n"
f"펌프 운전 교차검증 (area별):\n" + "\n".join(corr_lines) + "\n\n"
f"최근 이벤트 표본 (최대 40건):\n" + "\n".join(recent_lines)
)
@@ -1766,6 +1796,7 @@ async def generate_status_report(area: str | None = None, hours: int = 24) -> st
"active_alarms_count": len(alarms),
"recent_events_count": len(events),
"by_type": by_type,
"pump_corroboration": pump_corr,
"window_hours": hours,
"area": area,
},