hee_!J commited on
Commit ·
8a48888
0
Parent(s):
chore: FabAgent 골격 구성
Browse files- .env.example +3 -0
- .gitignore +13 -0
- README.md +74 -0
- agents/__init__.py +0 -0
- agents/cause.py +11 -0
- agents/detection.py +11 -0
- agents/impact.py +11 -0
- agents/orchestrator.py +20 -0
- agents/rag/__init__.py +0 -0
- agents/rag/knowledge/README.md +15 -0
- agents/rag/store.py +22 -0
- agents/response.py +11 -0
- app.py +68 -0
- components/__init__.py +0 -0
- components/alarm_inbox.py +13 -0
- components/header.py +11 -0
- components/progress.py +11 -0
- components/skeleton.py +12 -0
- components/tiers.py +28 -0
- components/tiers_body.py +25 -0
- core/__init__.py +0 -0
- core/pipeline.py +22 -0
- core/schema.py +83 -0
- data/README.md +25 -0
- data/__init__.py +0 -0
- data/demo.py +106 -0
- data/secom/__init__.py +0 -0
- data/secom/loader.py +33 -0
- data/secom/raw/.gitkeep +0 -0
- requirements.txt +4 -0
- styles/main.css +42 -0
- tests/__init__.py +0 -0
.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
|