hee_!J commited on
Commit
8a48888
·
0 Parent(s):

chore: FabAgent 골격 구성

Browse files
.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # OpenAI API — 백엔드 멀티에이전트(agents/)용
2
+ # 서브에이전트 = GPT-5 mini, 오케스트레이터 = GPT-5
3
+ OPENAI_API_KEY=sk-...
.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ .env
4
+ .venv/
5
+ venv/
6
+ .streamlit/secrets.toml
7
+ .DS_Store
8
+ .idea/
9
+ .vscode/
10
+ CLAUDE.md
11
+ .claude/
12
+ .mcp.json
13
+ data/secom/raw/*.data
README.md ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FabAgent
2
+
3
+ 반도체 공정 이상의 **탐지 → 원인 분석 → 영향 평가 → 대응 권고**를 하나의
4
+ 멀티 에이전트 파이프라인으로 통합하는 운영 플랫폼 MVP입니다.
5
+
6
+ ## 아키텍처
7
+
8
+ ```
9
+ 알람 클릭 → core/pipeline.get_tier_data(alarm_id)
10
+ ├─ A1 (Photo): agents/ 실제 LLM 멀티에이전트 + RAG
11
+ │ └ Tier 1은 SECOM 센서 데이터로 이상 탐지
12
+ └─ A2·A3: data/demo.py 하드코딩
13
+ → core/schema.TierData (단일 계약면)
14
+ → components/ 가 Streamlit으로 렌더
15
+ ```
16
+
17
+ 프론트엔드는 데이터가 실제 에이전트에서 오는지 하드코딩인지 알지 못합니다.
18
+ 실제 에이전트로 돌릴 알람을 늘리려면 `core/pipeline.REAL_AGENT_ALARMS`에
19
+ ID만 추가하면 됩니다.
20
+
21
+ ## 파일 구조
22
+
23
+ ```
24
+ fabagent/
25
+ ├── app.py # 엔트리포인트
26
+ ├── components/ # Streamlit UI (프론트 담당)
27
+ ├── core/
28
+ │ ├── schema.py # Tier1~4 데이터 계약 ★ 양쪽 공유
29
+ │ └── pipeline.py # 알람 → Tier 데이터 라우터
30
+ ├── data/
31
+ │ ├── demo.py # A2·A3 하드코딩 데이터
32
+ │ └── secom/ # UCI SECOM 데이터셋 + 로더 (Tier 1 이상 탐지용)
33
+ ├── agents/ # A1 실제 멀티에이전트 (백엔드 담당)
34
+ │ ├── orchestrator.py
35
+ │ ├── detection.py / cause.py / impact.py / response.py
36
+ │ └── rag/ # 도메인 지식 검색
37
+ ├── styles/main.css # 디자인 시안 CSS
38
+ └── assets/
39
+ ```
40
+
41
+ ## 기술 스택
42
+
43
+ - **프론트**: Streamlit 1.36+ (의존성 최소화 — `streamlit-extras` 등 미사용)
44
+ - **백엔드**: OpenAI SDK — 서브에이전트 `GPT-5 mini`, 오케스트레이터 `GPT-5`
45
+ - **데이터**: UCI SECOM 공개 데이터셋 (Tier 1 이상 탐지), pandas
46
+ - **Python**: 3.11+
47
+
48
+ 데이터셋 준비 방법은 `data/README.md`를 참고하세요.
49
+
50
+ ## 실행
51
+
52
+ ```bash
53
+ pip install -r requirements.txt
54
+ cp .env.example .env # OPENAI_API_KEY 입력
55
+ streamlit run app.py --server.port 8501
56
+ ```
57
+
58
+ ## 개발 마일스톤
59
+
60
+ | 단계 | 내용 | 담당 |
61
+ |---|---|---|
62
+ | M0 | 레포 골격 · `schema.py` 합의 · 설정 파일 | 같이 |
63
+ | M1 | Streamlit UI 전체 (`demo.py` 데이터로) | 프론트 |
64
+ | M2 | `agents/` — A1 4단계 오케스트레이션 + RAG | 백엔드 |
65
+ | M3 | A1을 실제 에이전트로 스위치 · 통합 테스트 | 같이 |
66
+ | M4 | 시연 리허설 (개발 가이드 15장 체크리스트) | 같이 |
67
+
68
+ M1·M2는 `core/schema.py`만 고정되면 완전히 병렬로 작업할 수 있습니다.
69
+
70
+ ## 브랜치 전략
71
+
72
+ - 기본 브랜치는 `develop`이며, 직접 푸시하지 않고 PR로만 머지합니다.
73
+ - `feat/streamlit-ui` · `feat/agents-a1` · `feat/css-port` 로 작업을 분담합니다.
74
+ - `core/schema.py`를 가장 먼저 작은 PR로 머지한 뒤 병렬 작업을 시작합니다.
agents/__init__.py ADDED
File without changes
agents/cause.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tier 2 원인 분석 에이전트 (M2에서 구현)
2
+
3
+ 입력: 알람 컨텍스트 + Tier 1 결과
4
+ 출력: core.schema.Tier2 (추정 원인 + 기여도 % + 근거 + RAG citation)
5
+ 모델: GPT-5 mini + RAG (agents.rag.store), 과거 인시던트/FMEA 문서 검색
6
+ """
7
+ from core.schema import Tier1, Tier2
8
+
9
+
10
+ def run_cause(alarm: dict, tier1: Tier1) -> Tier2:
11
+ raise NotImplementedError("M2: Tier 2 원인 분석 에이전트 구현 예정")
agents/detection.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tier 1 이상 탐지 에이전트 (M2에서 구현)
2
+
3
+ 입력: 알람 컨텍스트 + SECOM 센서 데이터 (data.secom.loader.load_secom)
4
+ 출력: core.schema.Tier1 (이상 점수 + 기여 피처 Top-N + 영향 lot)
5
+ 모델: GPT-5 mini + tool use, IsolationForest 등으로 이상 점수 계산
6
+ """
7
+ from core.schema import Tier1
8
+
9
+
10
+ def run_detection(alarm: dict) -> Tier1:
11
+ raise NotImplementedError("M2: Tier 1 이상 탐지 에이전트 구현 예정")
agents/impact.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tier 3 공정 간 영향 평가 에이전트 (M2에서 구현)
2
+
3
+ 입력: 알람 컨텍스트 + Tier 1·2 결과
4
+ 출력: core.schema.Tier3 (예상 수율 손실 + 공정 의존성 + 영향 lot)
5
+ 모델: GPT-5 mini + tool use (후공정 의존성 그래프 조회 툴)
6
+ """
7
+ from core.schema import Tier1, Tier2, Tier3
8
+
9
+
10
+ def run_impact(alarm: dict, tier1: Tier1, tier2: Tier2) -> Tier3:
11
+ raise NotImplementedError("M2: Tier 3 영향 평가 에이전트 구현 예정")
agents/orchestrator.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """4-Tier 멀티에이전트 오케스트레이터 (M2에서 구현)
2
+
3
+ run_orchestrator()가 detection -> cause -> impact -> response 순으로
4
+ 서브에이전트를 호출하고 각 결과를 core.schema.TierData로 합쳐 반환
5
+
6
+ 설계 원칙: 특정 알람(A1)에 하드코딩하지 말 것
7
+ 알람의 공정 타입(Photo/Etch/CMP)을 받아 처리하는 일반 함수로 만들면
8
+ core.pipeline.REAL_AGENT_ALARMS에 ID만 추가해 다른 알람도 확장 가능
9
+
10
+ 모델: 서브에이전트 = GPT-5 mini, 오케스트레이터 = GPT-5 (OpenAI SDK)
11
+
12
+ 현재는 스텁, demo.py 데이터를 그대로 반환해 프론트가 M1부터 동작하게 함
13
+ """
14
+ from core.schema import TierData
15
+ from data import demo
16
+
17
+
18
+ def run_orchestrator(alarm_id: str) -> TierData:
19
+ # TODO(M2/M3) detection->cause->impact->response 서브에이전트 호출로 교체
20
+ return demo.TIER_DATA[alarm_id]
agents/rag/__init__.py ADDED
File without changes
agents/rag/knowledge/README.md ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RAG 지식 베이스
2
+
3
+ 원인 분석(Tier 2)·대응 권고(Tier 4) 에이전트가 검색하는 도메인 문서.
4
+ 파일명 = citation ID. 예: `INC-2024-0312.md` → 코드에서 `load_document("INC-2024-0312")`.
5
+
6
+ ## M2에서 작성할 문서 (A1 Photo 시나리오 기준)
7
+
8
+ | 파일 | 유형 | 내용 |
9
+ |---|---|---|
10
+ | `INC-2024-0312.md` | 과거 인시던트 | 메모리 1동 렌즈 오염 유사 사례 |
11
+ | `INC-2024-0289.md` | 과거 인시던트 | 스테이지 진동 이상 사례 |
12
+ | `FMEA-PH-007.md` | 실패 모드 분석 | Photo 공정 FMEA |
13
+ | `SOP-PH-LENS-002.md` | 표준 절차 | 렌즈 PM 표준 절차 |
14
+
15
+ A2·A3까지 실제 에이전트로 확장할 경우 Etch/CMP 관련 문서도 같은 형식으로 추가.
agents/rag/store.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """RAG 도메인 지식 검색 (M2에서 구현)
2
+
3
+ knowledge/ 의 마크다운 문서(INC-*, FMEA-*, SOP-* 등)를 검색해
4
+ 원인 분석(Tier 2)·대응 권고(Tier 4) 에이전트에 근거를 제공
5
+
6
+ 문서가 5~6개뿐이라 벡터DB는 오버스펙
7
+ citation ID로 직접 로드하거나 간단한 키워드 매칭이면 충분
8
+ """
9
+ from pathlib import Path
10
+
11
+ KNOWLEDGE_DIR = Path(__file__).parent / "knowledge"
12
+
13
+
14
+ def load_document(doc_id: str) -> str:
15
+ """citation ID(예: 'INC-2024-0312')로 knowledge 문서 본문을 로드, 없으면 빈 문자열"""
16
+ path = KNOWLEDGE_DIR / f"{doc_id}.md"
17
+ return path.read_text(encoding="utf-8") if path.exists() else ""
18
+
19
+
20
+ def search(query: str, top_k: int = 3) -> list[str]:
21
+ """쿼리와 관련된 문서 ID를 반환 (M2: 키워드 매칭 기반)"""
22
+ raise NotImplementedError("M2: RAG 검색 구현 예정")
agents/response.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tier 4 대응 권고 에이전트 (M2에서 구현)
2
+
3
+ 입력: 알람 컨텍스트 + Tier 1·2·3 결과
4
+ 출력: core.schema.Tier4 (즉시 조치 + 중장기 조치 + 근거 자료)
5
+ 모델: GPT-5 (오케스트레이터급) + RAG, SOP/표준 절차 문서 검색
6
+ """
7
+ from core.schema import Tier1, Tier2, Tier3, Tier4
8
+
9
+
10
+ def run_response(alarm: dict, tier1: Tier1, tier2: Tier2, tier3: Tier3) -> Tier4:
11
+ raise NotImplementedError("M2: Tier 4 대응 권고 에이전트 구현 예정")
app.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FabAgent 운영자 대시보드 엔트리포인트
2
+
3
+ 실행: streamlit run app.py --server.port 8501
4
+ """
5
+ from pathlib import Path
6
+
7
+ import streamlit as st
8
+
9
+ from components.alarm_inbox import render_alarm_inbox
10
+ from components.header import render_header
11
+ from components.progress import render_progress_strip
12
+ from components.tiers import render_tier_cascade
13
+ from data.demo import DEFAULT_ALARMS
14
+
15
+ st.set_page_config(
16
+ page_title="FabAgent — 운영자 대시보드",
17
+ page_icon="🟦",
18
+ layout="wide",
19
+ initial_sidebar_state="expanded",
20
+ )
21
+
22
+
23
+ def inject_css():
24
+ css_path = Path("styles/main.css")
25
+ if css_path.exists():
26
+ st.markdown(
27
+ f"<style>{css_path.read_text(encoding='utf-8')}</style>",
28
+ unsafe_allow_html=True,
29
+ )
30
+ st.markdown(
31
+ '<link rel="stylesheet" '
32
+ 'href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css">',
33
+ unsafe_allow_html=True,
34
+ )
35
+
36
+
37
+ def init_state():
38
+ ss = st.session_state
39
+ ss.setdefault("selected_alarm_id", "A1")
40
+ ss.setdefault("stage", 0) # 0=idle, 1..4=loading, 5=done
41
+ ss.setdefault("completed_tiers", set())
42
+ ss.setdefault("approved", False)
43
+ ss.setdefault("alarms", [a.copy() for a in DEFAULT_ALARMS])
44
+ ss.setdefault("animation_pending", False)
45
+ ss.setdefault("speed", "normal") # "fast" | "normal" | "real"
46
+
47
+
48
+ def render_main():
49
+ render_header()
50
+
51
+ col_title, col_progress = st.columns([5, 4])
52
+ with col_title:
53
+ # TODO(M1) 메인 타이틀 + 4-Tier 워크플로우 서브텍스트
54
+ st.markdown("<!-- TODO(M1): 메인 타이틀 -->", unsafe_allow_html=True)
55
+ with col_progress:
56
+ render_progress_strip()
57
+
58
+ st.markdown(
59
+ '<hr style="border-color: var(--border); margin: 16px 0 22px;"/>',
60
+ unsafe_allow_html=True,
61
+ )
62
+ render_tier_cascade()
63
+
64
+
65
+ inject_css()
66
+ init_state()
67
+ render_alarm_inbox()
68
+ render_main()
components/__init__.py ADDED
File without changes
components/alarm_inbox.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """사이드바 알람 인박스 (개발 가이드 6장)
2
+
3
+ TODO(M1): 가이드 6장 '권장 대안', HTML 카드 + 바로 아래 st.button 분리 방식
4
+ on_alarm_click()에서 stage / completed_tiers / approved 리셋 후
5
+ animation_pending=True 로 두고 st.rerun()
6
+ """
7
+ import streamlit as st
8
+
9
+
10
+ def render_alarm_inbox():
11
+ with st.sidebar:
12
+ # TODO(M1) st.session_state.alarms 순회하며 알람 카드 + 선택 버튼 렌더
13
+ st.markdown("<!-- TODO(M1): 알람 인박스 -->", unsafe_allow_html=True)
components/header.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """상단 헤더 (개발 가이드 부록 A)
2
+
3
+ TODO(M1): 가이드 부록 A의 render_header() 구현, 디자인 시안 헤더와 1:1
4
+ 헤더는 st.columns 밖, .block-container 최상단에 둠
5
+ """
6
+ import streamlit as st
7
+
8
+
9
+ def render_header():
10
+ # TODO(M1) 로고 + 공정/라인 컨텍스트 + LIVE 시계 + 사용자 영역
11
+ st.markdown("<!-- TODO(M1): FabAgent 헤더 -->", unsafe_allow_html=True)
components/progress.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """4단계 진행 스트립 (개발 가이드 부록 C)
2
+
3
+ TODO(M1): 가이드 부록 C의 render_progress_strip() 구현
4
+ st.session_state.stage 기준으로 각 단계를 idle / active / done 으로 렌더
5
+ """
6
+ import streamlit as st
7
+
8
+
9
+ def render_progress_strip():
10
+ # TODO(M1) 이상 탐지 -> 원인 분석 -> 영향 평가 -> 권고서 진행 표시
11
+ st.markdown("<!-- TODO(M1): 진행 스트립 -->", unsafe_allow_html=True)
components/skeleton.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """로딩 스켈레톤 (개발 가이드 12장)
2
+
3
+ TODO(M1): 가이드 12장 render_skeleton() 구현
4
+ 스켈레톤 CSS(.skel, .skel-row, .skel-block, @keyframes shimmer)는
5
+ 디자인 프로토타입에서 styles/main.css로 복사
6
+ """
7
+ import streamlit as st
8
+
9
+
10
+ def render_skeleton(tier_num: int):
11
+ # TODO(M1) tier_num별 스켈레톤 레이아웃
12
+ st.markdown("<!-- TODO(M1): 스켈레톤 -->", unsafe_allow_html=True)
components/tiers.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tier 1~4 순차 등장 렌더러 (개발 가이드 8·9장)
2
+
3
+ TODO(M1): 가이드 8장 '접근법 A', st.empty() placeholder 4개 + time.sleep으로
4
+ 스켈레톤 -> 콘텐츠 순차 교체, stage>=5면 즉시 정적 렌더(새로고침 대응)
5
+
6
+ 중요: Tier 데이터는 반드시 core.pipeline.get_tier_data() 한 함수로만 받음
7
+ data.demo.TIER_DATA 를 직접 import 하지 말 것, 실제 에이전트 교체가 막힘
8
+ """
9
+ import streamlit as st
10
+
11
+ from core.pipeline import get_tier_data
12
+
13
+
14
+ def render_tier_cascade():
15
+ data = get_tier_data(st.session_state.selected_alarm_id)
16
+ if data is None:
17
+ st.markdown(
18
+ '<div class="fab-empty">선택한 알람의 분석 데이터가 없습니다.</div>',
19
+ unsafe_allow_html=True,
20
+ )
21
+ return
22
+ # TODO(M1) 가이드 8장 run_sequence(), 스켈레톤 -> 콘텐츠 순차 등장
23
+ st.markdown("<!-- TODO(M1): Tier 1~4 cascade -->", unsafe_allow_html=True)
24
+
25
+
26
+ def render_tier(tier_num: int, data, loading: bool, with_actions: bool = False):
27
+ # TODO(M1) 가이드 9장 render_tier(), Tier 프레임 + 본문/스켈레톤 분기
28
+ raise NotImplementedError("M1: Tier 프레임 렌더러 구현 예정")
components/tiers_body.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tier 1~4 본문 렌더러 (개발 가이드 9·10장)
2
+
3
+ TODO(M1): 상호작용 없는 본문은 f-string HTML + st.markdown 한 번에 렌더
4
+ Streamlit 위젯을 본문에 쓰지 말 것(레이아웃 깨짐), Tier 4 액션 바만 st.button
5
+ 디자인 프로토타입의 tiers.jsx 구조를 1:1로 옮길 것
6
+ """
7
+ from core.schema import Tier1, Tier2, Tier3, Tier4
8
+
9
+
10
+ def render_tier_1_body(data: Tier1):
11
+ raise NotImplementedError("M1: Tier 1 본문 구현 예정 (가이드 9장 참고)")
12
+
13
+
14
+ def render_tier_2_body(data: Tier2):
15
+ raise NotImplementedError("M1: Tier 2 본문 구현 예정")
16
+
17
+
18
+ def render_tier_3_body(data: Tier3):
19
+ raise NotImplementedError("M1: Tier 3 본문 구현 예정")
20
+
21
+
22
+ def render_tier_4_body(data: Tier4, with_actions: bool):
23
+ # TODO(M1) 본문 HTML + with_actions면 거절/보류/승인 액션 바
24
+ # 승인 시 on_approve, approved=True, 알람 status="done", st.toast, st.rerun
25
+ raise NotImplementedError("M1: Tier 4 본문 + 액션 바 구현 예정 (가이드 10장)")
core/__init__.py ADDED
File without changes
core/pipeline.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """알람 -> Tier 데이터 라우터
2
+
3
+ components/는 이 함수 하나만 호출함
4
+ 데이터가 하드코딩(demo.py)에서 오는지 실제 멀티에이전트(agents/)에서
5
+ 오는지 프론트는 알 필요가 없음
6
+
7
+ 실제 LLM 에이전트로 돌릴 알람을 늘리려면 REAL_AGENT_ALARMS에 ID만 추가하면 됨
8
+ """
9
+ from core.schema import TierData
10
+ from data import demo
11
+
12
+ # LLM 멀티에이전트로 처리할 알람
13
+ REAL_AGENT_ALARMS = {"A1"}
14
+
15
+
16
+ def get_tier_data(alarm_id: str) -> TierData | None:
17
+ """알람 ID로 4-Tier 분석 결과를 반환, 데이터가 없으면 None"""
18
+ if alarm_id in REAL_AGENT_ALARMS:
19
+ from agents.orchestrator import run_orchestrator
20
+
21
+ return run_orchestrator(alarm_id)
22
+ return demo.TIER_DATA.get(alarm_id)
core/schema.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FabAgent Tier 데이터 계약
2
+
3
+ 프론트엔드(components/)와 백엔드(agents/)가 공유하는 단일 계약면
4
+ 이 구조를 바꾸면 양쪽 다 영향받으므로, 변경은 작은 PR로 먼저 합의 예정
5
+
6
+ 모든 Tier 데이터는 plain dict로 다룸 - components/가 HTML f-string에서
7
+ dict 키로 직접 접근하기 때문, TypedDict는 타입 힌트/문서 용도
8
+ """
9
+ from typing import Optional, TypedDict
10
+
11
+
12
+ # Tier 1 이상 탐지
13
+ class Feature(TypedDict):
14
+ name: str
15
+ value: float
16
+
17
+
18
+ class LotRef(TypedDict):
19
+ id: str
20
+ wafers: int
21
+
22
+
23
+ class Tier1(TypedDict):
24
+ score: float # 이상 점수 (0~1)
25
+ features: list[Feature] # 기여 피처 Top-N (value 내림차순)
26
+ lot: LotRef
27
+
28
+
29
+ # Tier 2 원인 분석
30
+ class Cause(TypedDict):
31
+ name: str
32
+ pct: int # 추정 기여도 (%)
33
+ evidence: str
34
+ citations: list[str] # RAG 근거 문서 ID
35
+
36
+
37
+ class Tier2(TypedDict):
38
+ causes: list[Cause]
39
+
40
+
41
+ # Tier 3 공정 간 영향 평가
42
+ class Dependency(TypedDict):
43
+ stage: str
44
+ delta: str
45
+ tag: str
46
+ kind: str # "current" | "impacted" | "minor"
47
+
48
+
49
+ class ImpactLot(TypedDict):
50
+ label: str
51
+ lots: int
52
+ wafers: int
53
+
54
+
55
+ class Tier3(TypedDict):
56
+ yield_loss: float # 예상 수율 손실 (%p)
57
+ dependencies: list[Dependency]
58
+ impact_lots: list[ImpactLot]
59
+
60
+
61
+ # Tier 4 대응 권고
62
+ class Action(TypedDict):
63
+ text: str
64
+ meta: Optional[str]
65
+
66
+
67
+ class Reference(TypedDict):
68
+ id: str
69
+ desc: str
70
+
71
+
72
+ class Tier4(TypedDict):
73
+ immediate: list[Action]
74
+ longterm: list[Action]
75
+ refs: list[Reference]
76
+
77
+
78
+ # 전체
79
+ class TierData(TypedDict):
80
+ tier1: Tier1
81
+ tier2: Tier2
82
+ tier3: Tier3
83
+ tier4: Tier4
data/README.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 데이터
2
+
3
+ ## SECOM (Tier 1 이상 탐지용)
4
+
5
+ UCI SECOM 반도체 제조 공정 센서 데이터셋을 사용합니다.
6
+
7
+ - 규모: 1,567 row × 590 sensor feature, pass/fail 라벨
8
+ - 출처: https://archive.ics.uci.edu/dataset/179/secom
9
+ - Tier 1 이상 탐지 에이전트가 이 데이터로 이상 점수와 기여 피처를 계산합니다.
10
+
11
+ ### 준비 방법
12
+
13
+ `secom.data`, `secom_labels.data` 두 파일을 `data/secom/raw/` 에 둡니다.
14
+ raw 데이터는 git에 포함하지 않으므로(`.gitignore`) 각자 내려받아야 합니다.
15
+
16
+ 로드는 `data/secom/loader.py`의 `load_secom()`을 사용합니다.
17
+
18
+ ### 알람 매핑
19
+
20
+ SECOM은 익명화된 데이터라 lot ID·공정 step 정보가 없습니다. MVP에서는 fail 라벨이
21
+ 붙은 특정 row 하나를 알람 A1(Photo Step 이상)의 대상 웨이퍼로 지정해 사용합니다.
22
+
23
+ ## demo.py (A2·A3용)
24
+
25
+ A2·A3는 실제 데이터 대신 시연용 하드코딩 데이터를 사용합니다 (`data/demo.py`).
data/__init__.py ADDED
File without changes
data/demo.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """데모 데이터 - 알람 인박스 + 하드코딩 Tier 데이터
2
+
3
+ A1(Photo)은 M3에서 agents/ 실제 에이전트로 교체됨, A2·A3는 시연용 하드코딩
4
+ TIER_DATA에 없는 알람을 클릭하면 pipeline이 None을 반환하고
5
+ 프론트가 "데이터 없음"을 안내함
6
+ """
7
+ from core.schema import TierData
8
+
9
+ DEFAULT_ALARMS = [
10
+ {
11
+ "id": "A1",
12
+ "status": "critical",
13
+ "title": "Photo Step 이상",
14
+ "lot_id": "L20240511-N-03",
15
+ "feature": "CD-X 산포",
16
+ "feature_arrow": "↑",
17
+ "time": "3분 전",
18
+ },
19
+ {
20
+ "id": "A2",
21
+ "status": "warn",
22
+ "title": "Etch Step 이상",
23
+ "lot_id": "L20240511-N-02",
24
+ "feature": "Trench Depth",
25
+ "feature_arrow": "↓",
26
+ "time": "15분 전",
27
+ },
28
+ {
29
+ "id": "A3",
30
+ "status": "done",
31
+ "title": "CMP Step 이상",
32
+ "lot_id": "L20240510-N-08",
33
+ "feature": None,
34
+ "time": "2시간 전",
35
+ },
36
+ ]
37
+
38
+ STATUS_LABELS = {"critical": "긴급", "warn": "주의", "done": "완료"}
39
+
40
+ # alarm_id -> TierData
41
+ # A1은 개발 가이드 11장 데이터, A2·A3은 M1에서 채움
42
+ TIER_DATA: dict[str, TierData] = {
43
+ "A1": {
44
+ "tier1": {
45
+ "score": 0.87,
46
+ "features": [
47
+ {"name": "CD-X 산포", "value": 0.42},
48
+ {"name": "노광 에너지", "value": 0.31},
49
+ {"name": "Focus 편차", "value": 0.14},
50
+ ],
51
+ "lot": {"id": "L20240511-N-03", "wafers": 25},
52
+ },
53
+ "tier2": {
54
+ "causes": [
55
+ {
56
+ "name": "렌즈 오염",
57
+ "pct": 62,
58
+ "evidence": "직전 PM 후 14일 경과 · 유사 사례 4건 · 헤이즈 센서 +18%",
59
+ "citations": ["INC-2024-0312", "FMEA-PH-007"],
60
+ },
61
+ {
62
+ "name": "스테이지 진동",
63
+ "pct": 23,
64
+ "evidence": "동일 시간대 진동 센서 이상치 검출",
65
+ "citations": ["INC-2024-0289"],
66
+ },
67
+ {
68
+ "name": "웨이퍼 표면 결함",
69
+ "pct": 15,
70
+ "evidence": "직전 공정 입고 검사 패스",
71
+ "citations": [],
72
+ },
73
+ ],
74
+ },
75
+ "tier3": {
76
+ "yield_loss": 2.3,
77
+ "dependencies": [
78
+ {"stage": "Photo", "delta": "+0.87", "tag": "현재", "kind": "current"},
79
+ {"stage": "Etch", "delta": "+18%", "tag": "영향", "kind": "impacted"},
80
+ {"stage": "CMP", "delta": "+5%", "tag": "경미", "kind": "minor"},
81
+ ],
82
+ "impact_lots": [
83
+ {"label": "가공 중", "lots": 3, "wafers": 75},
84
+ {"label": "대기 중", "lots": 5, "wafers": 125},
85
+ ],
86
+ },
87
+ "tier4": {
88
+ "immediate": [
89
+ {"text": "렌즈 PM 긴급 투입", "meta": "예상 2시간"},
90
+ {"text": "후공정 진입 보류 — 영향 lot 3건 (75장)", "meta": "Etch hold"},
91
+ {"text": "양산 일정 재조정 — 영향 범위 격리", "meta": "PPC 협조"},
92
+ ],
93
+ "longterm": [
94
+ {"text": "PM 주기 단축 권고: 30일 → 21일", "meta": None},
95
+ {"text": "동일 패턴 발생 lot 추적 모니터링 강화", "meta": None},
96
+ ],
97
+ "refs": [
98
+ {"id": "SOP-PH-LENS-002", "desc": "렌즈 PM 표준 절차"},
99
+ {"id": "INC-2024-0312", "desc": "과거 유사 사례 (메모리 1동)"},
100
+ {"id": "FMEA-PH-007", "desc": "Photo 공정 실패 모드 분석"},
101
+ ],
102
+ },
103
+ },
104
+ # TODO(M1) A2(Etch) 시연 데이터 추가
105
+ # TODO(M1) A3(CMP) 시연 데이터 추가, 비워두면 "데이터 없음" 경로가 시연됨
106
+ }
data/secom/__init__.py ADDED
File without changes
data/secom/loader.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """UCI SECOM 데이터셋 로더
2
+
3
+ 반도체 제조 공정 센서 데이터 (1567 row x 590 feature, pass/fail 라벨)
4
+ 출처: https://archive.ics.uci.edu/dataset/179/secom
5
+ raw/ 에 secom.data, secom_labels.data 를 두면 로드됨 (data/README.md 참고)
6
+
7
+ Tier 1 이상 탐지 에이전트가 이 데이터로 이상 점수와 기여 피처를 계산
8
+ """
9
+ from pathlib import Path
10
+
11
+ import pandas as pd
12
+
13
+ RAW_DIR = Path(__file__).parent / "raw"
14
+
15
+
16
+ def load_secom() -> tuple[pd.DataFrame, pd.Series]:
17
+ """SECOM 센서 피처와 pass/fail 라벨을 반환
18
+
19
+ features: 1567 x 590 (결측치 포함), 컬럼명 sensor_000 ~ sensor_589
20
+ labels: 1=fail(이상), -1=pass(정상)
21
+ """
22
+ data_path = RAW_DIR / "secom.data"
23
+ label_path = RAW_DIR / "secom_labels.data"
24
+ if not data_path.exists() or not label_path.exists():
25
+ raise FileNotFoundError(
26
+ f"SECOM 데이터가 없음, {RAW_DIR}에 secom.data / secom_labels.data 를 두세요 "
27
+ "(data/README.md 참고)"
28
+ )
29
+
30
+ features = pd.read_csv(data_path, sep=r"\s+", header=None)
31
+ features.columns = [f"sensor_{i:03d}" for i in range(features.shape[1])]
32
+ labels = pd.read_csv(label_path, sep=r"\s+", header=None, usecols=[0])[0]
33
+ return features, labels
data/secom/raw/.gitkeep ADDED
File without changes
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ streamlit>=1.36.0
2
+ openai>=1.0.0
3
+ python-dotenv>=1.0.0
4
+ pandas>=2.0.0
styles/main.css ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* FabAgent 전체 스타일
2
+ TODO(M1 / feat/css-port): 디자인 프로토타입의 styles.css 전체를 여기에 그대로 복사
3
+ 토큰/간격/애니메이션이 모두 그 안에 검증돼 있으므로 다시 작성하지 말 것
4
+ 아래는 디자인 토큰 :root + Streamlit 기본 요소 숨김만 미리 둔 시작점 */
5
+
6
+ :root {
7
+ --text-primary: #1F2A3A;
8
+ --text-secondary: #6B7788;
9
+ --text-tertiary: #9AA3B2;
10
+ --bg-page: #FAFBFC;
11
+ --bg-card: #FFFFFF;
12
+ --bg-subtle: #F4F6F9;
13
+ --border: #E3E6EC;
14
+ --brand: #2C5AB8; /* Tier 1 파랑 */
15
+ --t2-text: #6E46A8; /* Tier 2 보라 */
16
+ --t3-text: #2A8A55; /* Tier 3 초록 */
17
+ --t4-text: #B07718; /* Tier 4 앰버 */
18
+ --crit-text: #C04A6E; /* 긴급 핑크 */
19
+ }
20
+
21
+ /* Streamlit 기본 요소 숨기기 */
22
+ #MainMenu { visibility: hidden; }
23
+ footer { visibility: hidden; }
24
+ header[data-testid="stHeader"] { display: none; }
25
+
26
+ /* 상단 여백 제거 + 폭 제한 해제 */
27
+ .block-container {
28
+ padding-top: 1rem;
29
+ padding-bottom: 4rem;
30
+ max-width: none;
31
+ }
32
+
33
+ /* 사이드바 너비 320px 고정 */
34
+ section[data-testid="stSidebar"] {
35
+ width: 320px !important;
36
+ min-width: 320px !important;
37
+ background: var(--bg-card);
38
+ border-right: 1px solid var(--border);
39
+ }
40
+ section[data-testid="stSidebar"] > div {
41
+ padding-top: 0;
42
+ }
tests/__init__.py ADDED
File without changes