fabagent / components /tiers.py
hee_!J
feat: μ—μ΄μ „νŠΈ κ°€μ‹œν™” κ°•ν™” (페λ₯΄μ†Œλ‚˜Β·SPC 차트·LangGraph)
6fb4d4f
Raw
History Blame Contribute Delete
9.59 kB
"""Tier 1~4 순차 λ“±μž₯ λ Œλ”λŸ¬ (개발 κ°€μ΄λ“œ 8Β·9Β·10μž₯)
st.empty() placeholder 4개 + time.sleep으둜 μŠ€μΌˆλ ˆν†€ β†’ μ½˜ν…μΈ  순차 ꡐ체
이미 μ™„λ£Œλœ μƒνƒœ(stage>=5)κ±°λ‚˜ animation_pending이 아닐 λ•ŒλŠ” μ¦‰μ‹œ 정적 λ Œλ”
β˜… Tier λ°μ΄ν„°λŠ” λ°˜λ“œμ‹œ core.pipeline.get_tier_data() ν•œ ν•¨μˆ˜λ‘œλ§Œ λ°›μŒ
data.demo.TIER_DATAλ₯Ό 직접 import ν•˜μ§€ 말 것 (μ‹€μ œ μ—μ΄μ „νŠΈ ꡐ체가 λ§‰νž˜)
"""
import time
from datetime import datetime
import streamlit as st
from components.skeleton import skeleton_html
from components.tiers_body import (
tier_1_body_html,
tier_2_body_html,
tier_3_body_html,
tier_4_body_html,
)
from core.pipeline import get_tier_data
SPEED_PRESETS = {
"fast": {"skel": 0.6, "hold": 0.4},
"normal": {"skel": 1.4, "hold": 0.7},
"real": {"skel": 3.0, "hold": 1.5},
}
TIER_NAMES = {1: "이상 탐지", 2: "원인 뢄석", 3: "곡정 κ°„ 영ν–₯ 평가", 4: "λŒ€μ‘ ꢌ고"}
# 각 Tier의 μ—μ΄μ „νŠΈ 페λ₯΄μ†Œλ‚˜ + ν™œλ™ μš”μ•½ (loading=False일 λ•Œλ§Œ ν™œμ„±)
AGENT_PERSONA = {
1: {"name": "Detection Agent", "tool": "IsolationForest Β· SECOM/PHM"},
2: {"name": "Cause Agent", "tool": "GPT-5-mini Β· RAG 검색"},
3: {"name": "Impact Agent", "tool": "GPT-5-mini Β· RAG + WIP κ²°ν•©"},
4: {"name": "Response Agent", "tool": "GPT-5-mini Β· RAG + κ·Όκ±° μΆ”μΆœ"},
}
def _agent_summary(tier_num: int, data) -> str:
"""Tier별 μ—μ΄μ „νŠΈ ν™œλ™ 1쀄 μš”μ•½, dataκ°€ None이면 뢄석 쀑 λ©”μ‹œμ§€"""
if data is None:
return "뢄석 쀑..."
if tier_num == 1:
return f"{len(data['features'])}개 κΈ°μ—¬ μ„Όμ„œ κ²€μΆœ (이상 점수 {data['score']})"
if tier_num == 2:
cites = sum(len(c["citations"]) for c in data["causes"])
return f"사내 λ¬Έμ„œ {cites}건 μΈμš©ν•΄ 원인 {len(data['causes'])}개 μΆ”μ •"
if tier_num == 3:
return f"μ˜μ‘΄μ„± + WIP κ²°ν•©ν•΄ μ˜ˆμƒ 수율 손싀 {data['yield_loss']} %p μΆ”μ •"
if tier_num == 4:
return f"κ·Όκ±° {len(data['refs'])}건 μΈμš©ν•΄ μ¦‰μ‹œ {len(data['immediate'])}건 Β· 쀑μž₯κΈ° {len(data['longterm'])}건 ꢌ고"
return ""
BODY_BUILDERS = {
1: tier_1_body_html,
2: tier_2_body_html,
3: tier_3_body_html,
4: tier_4_body_html,
}
LOADING_HTML = """
<div class="fab-loading">
<div class="fab-loading-spinner"></div>
<h3 class="fab-loading-title">4-Tier λ©€ν‹° μ—μ΄μ „νŠΈ 뢄석 μ§„ν–‰ 쀑</h3>
<div class="fab-loading-subtitle">μ•ŒλžŒμ„ λΆ„μ„ν•˜κΈ° μœ„ν•΄ 4단계 μ—μ΄μ „νŠΈκ°€ 순차 μ‹€ν–‰λ©λ‹ˆλ‹€</div>
<div class="fab-loading-pipeline">
<span class="step t1">Tier 1 이상 탐지</span>
<span class="arrow">β†’</span>
<span class="step t2">Tier 2 원인 뢄석</span>
<span class="arrow">β†’</span>
<span class="step t3">Tier 3 영ν–₯ 평가</span>
<span class="arrow">β†’</span>
<span class="step t4">Tier 4 λŒ€μ‘ ꢌ고</span>
</div>
<div class="fab-loading-hint">첫 ν˜ΈμΆœμ€ LLM 3회 직렬둜 μ•½ 60초 μ†Œμš”λ©λ‹ˆλ‹€ Β· 이후 동일 μ•ŒλžŒμ€ μΊμ‹œλ‘œ μ¦‰μ‹œ 응닡</div>
</div>
"""
def render_tier_cascade():
ss = st.session_state
# 첫 호좜 μ‹œ LLM 3회 직렬둜 μ•½ 60초, μ‚¬μš©μž μ•ˆλ‚΄λ₯Ό μœ„ν•΄ λ‘œλ”© μΉ΄λ“œ λ¨Όμ € λ Œλ”
loading_slot = st.empty()
loading_slot.html(LOADING_HTML)
try:
data = get_tier_data(ss.selected_alarm_id)
finally:
loading_slot.empty()
if data is None:
st.markdown(
"""
<div class="fab-empty">
<div class="fab-empty-inner">
<div class="fab-empty-icon" style="font-size: 24px;">βˆ…</div>
<h3>뢄석 데이터가 μ—†μŠ΅λ‹ˆλ‹€</h3>
<p>μ„ νƒν•œ μ•ŒλžŒμ— λ§€ν•‘λœ 뢄석 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€</p>
</div>
</div>
""",
unsafe_allow_html=True,
)
return
if ss.animation_pending:
ss.animation_pending = False
_run_sequence(data)
return
# 정적 λ Œλ” (μž¬μ‹€ν–‰/μƒˆλ‘œκ³ μΉ¨ λŒ€μ‘)
for t in range(1, 5):
render_tier(t, data[f"tier{t}"], loading=False, with_actions=(t == 4))
def _run_sequence(data):
ss = st.session_state
timing = SPEED_PRESETS[ss.speed]
slots = [st.empty() for _ in range(4)]
for tier_idx in range(1, 5):
ss.stage = tier_idx
with slots[tier_idx - 1].container():
render_tier(tier_idx, None, loading=True)
time.sleep(timing["skel"])
with slots[tier_idx - 1].container():
render_tier(
tier_idx,
data[f"tier{tier_idx}"],
loading=False,
with_actions=(tier_idx == 4),
)
ss.completed_tiers.add(tier_idx)
if tier_idx < 4:
time.sleep(timing["hold"])
ss.stage = 5
def render_tier(tier_num: int, data, loading: bool, with_actions: bool = False):
"""Tier μΉ΄λ“œ ν•œ 덩어리λ₯Ό 단일 markdown으둜 λ Œλ”, Tier 4 μ•‘μ…˜ λ°”λ§Œ 별도 μœ„μ ―"""
status_html = (
'<div class="tier-status"><span class="spinner"></span>μ‹€ν–‰ 쀑...</div>'
if loading
else '<div class="tier-status"><span class="check">βœ“</span>μ™„λ£Œ</div>'
)
body_html = skeleton_html(tier_num) if loading else BODY_BUILDERS[tier_num](data)
p = AGENT_PERSONA[tier_num]
summary = _agent_summary(tier_num, None if loading else data)
st.html(
f'<section class="tier-card tier-{tier_num}">'
f'<div class="tier-head">'
f'<div class="tier-head-left">'
f'<div class="agent-id">'
f'<div class="agent-name-row">'
f'<span class="agent-name">{p["name"]}</span>'
f'<span class="chip chip-t{tier_num}">Tier {tier_num} Β· {TIER_NAMES[tier_num]}</span>'
f'</div>'
f'<div class="agent-summary">{summary} Β· <span class="agent-tool">{p["tool"]}</span></div>'
f'</div>'
f'</div>'
f'{status_html}'
f'</div>'
f'<div class="tier-body">{body_html}</div>'
f'</section>'
)
if tier_num == 4 and with_actions and not loading:
_render_action_bar()
def _render_action_bar():
ss = st.session_state
st.markdown(
'<div class="action-bar-head">'
'<span class="label">운영자 κ²°μ •</span>'
'<span>κΆŒκ³ μ„œλ₯Ό κ²€ν† ν•˜κ³  μ•‘μ…˜μ„ μ„ νƒν•˜μ„Έμš”</span>'
'</div>',
unsafe_allow_html=True,
)
# μ–‘μͺ½ spacer둜 κ°€μš΄λ° μ •λ ¬
_, c1, c2, c3, _ = st.columns([2.5, 1, 1, 2.2, 2.5])
with c1:
if st.button("거절", key="btn-reject", disabled=ss.approved, use_container_width=True):
_on_reject()
with c2:
if st.button("보λ₯˜", key="btn-hold", disabled=ss.approved, use_container_width=True):
_on_hold()
with c3:
label = "μž‘μ—…μ§€μ‹œμ„œ 생성됨" if ss.approved else "승인 및 μž‘μ—…μ§€μ‹œμ„œ 생성"
if st.button(label, key="btn-approve", disabled=ss.approved, type="primary", use_container_width=True):
_on_approve()
_render_action_result()
def _render_action_result():
ss = st.session_state
act = ss.get("last_action")
if not act:
return
kind = act["type"]
icon = {"approved": "βœ“", "held": "⏸", "rejected": "βœ—"}[kind]
title = act["title"]
meta = act["meta"]
st.html(
f'<div class="action-result {kind}">'
f'<div class="action-result-icon">{icon}</div>'
f'<div class="action-result-body">'
f'<div class="action-result-title">{title}</div>'
f'<div class="action-result-meta">{meta}</div>'
f'</div>'
f'</div>'
)
def _on_reject():
st.session_state.last_action = {
"type": "rejected",
"title": "ꢌ고 거절",
"meta": "μ‚¬μœ  μˆ˜μ§‘ λͺ¨λ‹¬μ€ MVP λ²”μœ„ μ™Έ, 후속 뢄석에 λ°˜μ˜ν•˜λ €λ©΄ μ‚¬μœ  μž…λ ₯ ν›„ μΈμ‹œλ˜νŠΈ DB에 기둝 ν•„μš”",
}
st.toast("ꢌ고 거절됨", icon="❌")
st.rerun()
def _on_hold():
st.session_state.last_action = {
"type": "held",
"title": "ꢌ고 보λ₯˜",
"meta": "5λΆ„ ν›„ 동일 μ•ŒλžŒμ΄ μž¬λ°œμƒ μ˜ˆμ •, μΆ”κ°€ 데이터 확보 ν›„ μž¬κ²€ν†  ꢌμž₯",
}
st.toast("ꢌ고 보λ₯˜λ¨", icon="⏸️")
st.rerun()
def _on_approve():
ss = st.session_state
ss.approved = True
# μžκ°€ ν•™μŠ΅ - ν˜„μž¬ μ•ŒλžŒμ˜ 뢄석 κ²°κ³Όλ₯Ό knowledge둜 μžλ™ 기둝
alarm = next((a for a in ss.alarms if a["id"] == ss.selected_alarm_id), None)
work_order = f"W-{datetime.now():%Y%m%d}-{ss.selected_alarm_id[1:].zfill(3)}"
incident_doc = ""
if alarm:
from agents.rag.learn import record_incident
tier_data = get_tier_data(ss.selected_alarm_id)
if tier_data:
path = record_incident(alarm, tier_data, work_order)
incident_doc = path.name
# μ•ŒλžŒ μƒνƒœ μ—…λ°μ΄νŠΈ
for a in ss.alarms:
if a["id"] == ss.selected_alarm_id:
a["status"] = "done"
a["time"] = "방금 μ „"
break
ss.last_action = {
"type": "approved",
"title": "승인 μ™„λ£Œ, μž‘μ—…μ§€μ‹œμ„œ λ°œν–‰",
"meta": (
f"μž‘μ—…μ§€μ‹œμ„œ <code>{work_order}</code> 생성 Β· "
f"μΈμ‹œλ˜νŠΈ DB <code>{incident_doc}</code> μžλ™ 기둝 Β· "
"μ‚¬μ΄λ“œλ°” μ•ŒλžŒ μƒνƒœκ°€ [μ™„λ£Œ]둜 μ „ν™˜λ˜μ—ˆμŠ΅λ‹ˆλ‹€"
),
}
st.toast(
f"βœ“ μž‘μ—… μ§€μ‹œμ„œ {work_order} 생성 Β· μΈμ‹œλ˜νŠΈ DB μžλ™ 기둝",
icon="βœ…",
)
st.rerun()