hee_!J commited on
Commit
d6ae565
·
1 Parent(s): 149d1d4

feat(tools): agent tool registry - 7개 tool + OpenAI function schema

Browse files
agents/tools/__init__.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Agent tool registry - LLM이 자율 호출하는 도구 모음
2
+
3
+ 각 tool은 (function, OpenAI schema) 쌍으로 구성
4
+ - knowledge.search_knowledge: hybrid RAG 검색
5
+ - incident.lookup_incident_history: 과거 incident 구조화 조회
6
+ - equipment.get_pm_history / check_pm_schedule: PM 이력·예정
7
+ - process.get_downstream_steps / query_wip_status / get_yield_baseline: 공정 의존성·WIP·수율
8
+
9
+ Tier별 사용 가능 tool 묶음:
10
+ - TOOLS_CAUSE: Tier 2 원인 분석 (search, incident, pm)
11
+ - TOOLS_IMPACT: Tier 3 영향 평가 (wip, downstream, yield, pm)
12
+ - TOOLS_RESPONSE: Tier 4 대응 권고 (search, incident, pm, schedule)
13
+
14
+ LLM 호출 패턴:
15
+ from agents.tools import TOOLS_CAUSE, dispatch_tool
16
+ resp = client().chat.completions.create(
17
+ model=..., messages=..., tools=TOOLS_CAUSE, tool_choice="auto"
18
+ )
19
+ for tc in resp.choices[0].message.tool_calls or []:
20
+ result = dispatch_tool(tc.function.name, json.loads(tc.function.arguments))
21
+ """
22
+ import json
23
+
24
+ from agents.tools import equipment, incident, knowledge, process
25
+
26
+ _REGISTRY = {
27
+ "search_knowledge": knowledge.search_knowledge,
28
+ "lookup_incident_history": incident.lookup_incident_history,
29
+ "get_pm_history": equipment.get_pm_history,
30
+ "check_pm_schedule": equipment.check_pm_schedule,
31
+ "get_downstream_steps": process.get_downstream_steps,
32
+ "query_wip_status": process.query_wip_status,
33
+ "get_yield_baseline": process.get_yield_baseline,
34
+ }
35
+
36
+ # Tier별 노출 tool 묶음
37
+ TOOLS_CAUSE = [
38
+ knowledge.SCHEMA,
39
+ incident.SCHEMA,
40
+ equipment.SCHEMA_GET_PM,
41
+ ]
42
+ TOOLS_IMPACT = [
43
+ process.SCHEMA_WIP,
44
+ process.SCHEMA_DOWNSTREAM,
45
+ process.SCHEMA_YIELD,
46
+ equipment.SCHEMA_GET_PM,
47
+ ]
48
+ TOOLS_RESPONSE = [
49
+ knowledge.SCHEMA,
50
+ incident.SCHEMA,
51
+ equipment.SCHEMA_GET_PM,
52
+ equipment.SCHEMA_CHECK_PM,
53
+ ]
54
+
55
+
56
+ def dispatch_tool(name: str, args: dict) -> str:
57
+ """tool 함수를 이름으로 호출, 결과를 JSON string으로 반환 (LLM 입력용)"""
58
+ fn = _REGISTRY.get(name)
59
+ if fn is None:
60
+ return json.dumps({"error": f"unknown tool: {name}"}, ensure_ascii=False)
61
+ try:
62
+ result = fn(**args)
63
+ except TypeError as e:
64
+ return json.dumps({"error": f"invalid args: {e}"}, ensure_ascii=False)
65
+ return json.dumps(result, ensure_ascii=False, default=str)
66
+
67
+
68
+ def all_tool_names() -> list[str]:
69
+ return list(_REGISTRY.keys())
agents/tools/equipment.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """장비 관련 tools - PM 이력 조회 + 가용 윈도우
2
+
3
+ 실 fab에선 EAP(Equipment Automation Program) / CIM 시스템에서 조회되는 값
4
+ 오늘 기준일은 2026-05-18 (데모 고정)
5
+ """
6
+ from datetime import date
7
+
8
+ TODAY = date(2026, 5, 18)
9
+
10
+ # 알람 ID -> 대표 장비 매핑
11
+ ALARM_EQUIPMENT = {
12
+ "A1": "ASML-PH-01",
13
+ "A2": "TEL-ET-03",
14
+ "A3": "AMAT-CMP-02",
15
+ }
16
+
17
+ _EQUIPMENT = {
18
+ "ASML-PH-01": {
19
+ "kind": "Photo Scanner (lens, stage)",
20
+ "process": "Photo",
21
+ "last_pm": date(2026, 5, 4),
22
+ "pm_cycle_days": 21,
23
+ "next_pm_windows": ["2026-05-19 02:00~06:00", "2026-05-21 22:00~02:00"],
24
+ },
25
+ "TEL-ET-03": {
26
+ "kind": "Etch Chamber",
27
+ "process": "Etch",
28
+ "last_pm": date(2026, 5, 11),
29
+ "pm_cycle_days": 30,
30
+ "next_pm_windows": ["2026-05-24 03:00~07:00"],
31
+ },
32
+ "AMAT-CMP-02": {
33
+ "kind": "CMP Polisher (slurry, pad)",
34
+ "process": "CMP",
35
+ "last_pm": date(2026, 4, 27),
36
+ "pm_cycle_days": 21,
37
+ "next_pm_windows": ["2026-05-19 04:00~07:00", "2026-05-20 23:00~03:00"],
38
+ },
39
+ "ASM-DIF-04": {
40
+ "kind": "Diffusion Furnace",
41
+ "process": "Diffusion",
42
+ "last_pm": date(2026, 4, 30),
43
+ "pm_cycle_days": 60,
44
+ "next_pm_windows": ["2026-05-25 06:00~10:00"],
45
+ },
46
+ }
47
+
48
+
49
+ def get_pm_history(equipment_id: str) -> dict:
50
+ """장비 마지막 PM 이력 + 경과일 + 다음 PM 예정일 반환
51
+
52
+ overdue: 다음 PM 예정일을 지났는가
53
+ """
54
+ eq = _EQUIPMENT.get(equipment_id)
55
+ if not eq:
56
+ return {"error": f"등록되지 않은 장비: {equipment_id}"}
57
+ days_since = (TODAY - eq["last_pm"]).days
58
+ overdue = days_since > eq["pm_cycle_days"]
59
+ return {
60
+ "equipment_id": equipment_id,
61
+ "kind": eq["kind"],
62
+ "process": eq["process"],
63
+ "last_pm_date": eq["last_pm"].isoformat(),
64
+ "days_since_pm": days_since,
65
+ "pm_cycle_days": eq["pm_cycle_days"],
66
+ "overdue": overdue,
67
+ "overdue_days": max(0, days_since - eq["pm_cycle_days"]),
68
+ }
69
+
70
+
71
+ def check_pm_schedule(equipment_id: str) -> dict:
72
+ """다음 7일 내 사용 가능한 PM 정비 윈도우 목록 반환"""
73
+ eq = _EQUIPMENT.get(equipment_id)
74
+ if not eq:
75
+ return {"error": f"등록되지 않은 장비: {equipment_id}"}
76
+ return {
77
+ "equipment_id": equipment_id,
78
+ "available_windows": eq["next_pm_windows"],
79
+ }
80
+
81
+
82
+ SCHEMA_GET_PM = {
83
+ "type": "function",
84
+ "function": {
85
+ "name": "get_pm_history",
86
+ "description": (
87
+ "장비의 마지막 PM(예방정비) 이력과 경과일, overdue 여부를 조회합니다. "
88
+ "원인 분석에서 'PM 누락이 원인인가' 판단할 때 활용하세요. "
89
+ "장비 ID는 알람별로 ASML-PH-01(A1), TEL-ET-03(A2), AMAT-CMP-02(A3) 가 매핑됩니다."
90
+ ),
91
+ "parameters": {
92
+ "type": "object",
93
+ "properties": {
94
+ "equipment_id": {
95
+ "type": "string",
96
+ "description": "장비 ID (예: 'ASML-PH-01')",
97
+ },
98
+ },
99
+ "required": ["equipment_id"],
100
+ },
101
+ },
102
+ }
103
+
104
+ SCHEMA_CHECK_PM = {
105
+ "type": "function",
106
+ "function": {
107
+ "name": "check_pm_schedule",
108
+ "description": (
109
+ "다음 7일 내 PM 정비 가능 윈도우를 조회합니다. "
110
+ "대응 권고에서 '언제 PM을 투입할 수 있는가'를 답할 때 사용하세요."
111
+ ),
112
+ "parameters": {
113
+ "type": "object",
114
+ "properties": {
115
+ "equipment_id": {
116
+ "type": "string",
117
+ "description": "장비 ID",
118
+ },
119
+ },
120
+ "required": ["equipment_id"],
121
+ },
122
+ },
123
+ }
agents/tools/incident.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """lookup_incident_history tool - 과거 incident 구조화 조회
2
+
3
+ knowledge/INC-*.md 의 RAG 검색과 별도로, 증상 키워드로 구조화 incident 레코드를 반환
4
+ (MES/incident DB 모방, 실 fab에선 Maximo/Splunk 등에서 조회)
5
+ """
6
+
7
+ _INCIDENTS = [
8
+ {
9
+ "id": "INC-2024-0312",
10
+ "date": "2024-03-12",
11
+ "process": "Photo",
12
+ "symptom": "CD-X 산포 증가",
13
+ "root_cause": "렌즈 오염",
14
+ "resolution": "긴급 PM 후 정상화 (2h 소요)",
15
+ "yield_recovery": "3.1% → 0.4%",
16
+ "keywords": ["cd", "산포", "렌즈", "오염", "photo", "노광"],
17
+ },
18
+ {
19
+ "id": "INC-2024-0289",
20
+ "date": "2024-02-28",
21
+ "process": "Photo",
22
+ "symptom": "스테이지 진동 이상치",
23
+ "root_cause": "스테이지 베어링 마모",
24
+ "resolution": "베어링 교체 후 alignment 재수행 (6h)",
25
+ "yield_recovery": "1.8% → 0.2%",
26
+ "keywords": ["스테이지", "진동", "베어링", "alignment", "photo"],
27
+ },
28
+ {
29
+ "id": "INC-CMP-2025-0142",
30
+ "date": "2025-08-14",
31
+ "process": "CMP",
32
+ "symptom": "MRR(재료 제거율) 산포 증가",
33
+ "root_cause": "슬러리 유량 변동",
34
+ "resolution": "슬러리 펌프 교체 + 유량 센서 재교정 (4h)",
35
+ "yield_recovery": "2.4% → 0.3%",
36
+ "keywords": ["mrr", "슬러리", "유량", "cmp", "재료", "제거율"],
37
+ },
38
+ {
39
+ "id": "INC-ET-2024-0301",
40
+ "date": "2024-03-01",
41
+ "process": "Etch",
42
+ "symptom": "트렌치 깊이 부족",
43
+ "root_cause": "식각 가스 유량 저하",
44
+ "resolution": "가스 라인 청소 + MFC 교정 (3h)",
45
+ "yield_recovery": "2.0% → 0.5%",
46
+ "keywords": ["트렌치", "깊이", "식각", "가스", "mfc", "etch"],
47
+ },
48
+ {
49
+ "id": "INC-2023-0892",
50
+ "date": "2023-11-22",
51
+ "process": "Photo",
52
+ "symptom": "Focus 편차 증가",
53
+ "root_cause": "스테이지 평탄도 이상",
54
+ "resolution": "스테이지 leveling 재수행 (2h)",
55
+ "yield_recovery": "1.5% → 0.3%",
56
+ "keywords": ["focus", "편차", "평탄도", "leveling", "photo"],
57
+ },
58
+ ]
59
+
60
+
61
+ def lookup_incident_history(symptom: str, max_results: int = 3) -> dict:
62
+ """증상 키워드로 과거 incident DB 조회, 일치 키워드 수로 랭킹
63
+
64
+ 반환: {"incidents": [{"id", "date", "process", "symptom", "root_cause",
65
+ "resolution", "yield_recovery"}, ...]}
66
+ """
67
+ q = symptom.lower()
68
+ q_tokens = [t for t in q.replace(",", " ").split() if len(t) >= 2]
69
+ scored = []
70
+ for inc in _INCIDENTS:
71
+ score = sum(1 for kw in inc["keywords"] if any(kw in t or t in kw for t in q_tokens))
72
+ if score > 0:
73
+ scored.append((score, inc))
74
+ scored.sort(key=lambda x: -x[0])
75
+ matched = [
76
+ {k: v for k, v in inc.items() if k != "keywords"}
77
+ for _, inc in scored[:max_results]
78
+ ]
79
+ return {"incidents": matched}
80
+
81
+
82
+ SCHEMA = {
83
+ "type": "function",
84
+ "function": {
85
+ "name": "lookup_incident_history",
86
+ "description": (
87
+ "과거 incident DB에서 증상 키워드와 일치하는 사례를 조회합니다. "
88
+ "구조화된 레코드(원인, 해결책, 소요시간, yield 회복률)를 반환하므로 "
89
+ "유사 사례 기반 의사결정에 활용하세요."
90
+ ),
91
+ "parameters": {
92
+ "type": "object",
93
+ "properties": {
94
+ "symptom": {
95
+ "type": "string",
96
+ "description": "증상 키워드 (예: 'CD 산포 증가', '슬러리 유량 이상')",
97
+ },
98
+ "max_results": {
99
+ "type": "integer",
100
+ "description": "최대 반환 건수 (기본 3)",
101
+ "default": 3,
102
+ },
103
+ },
104
+ "required": ["symptom"],
105
+ },
106
+ },
107
+ }
agents/tools/knowledge.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """search_knowledge tool - RAG 검색을 LLM이 자율 호출
2
+
3
+ 기존 agents.rag.store.search()를 tool 인터페이스로 노출
4
+ LLM이 추가 정보가 필요하다고 판단할 때 호출
5
+ """
6
+ from agents.rag.store import load_document, search
7
+
8
+
9
+ def search_knowledge(query: str, top_k: int = 3) -> dict:
10
+ """사내 지식 문서(INC/FMEA/SOP/FLOW)를 의미 + 키워드 hybrid 검색
11
+
12
+ 반환: {"hits": [{"doc_id": ..., "snippet": 첫 400자}, ...]}
13
+ """
14
+ doc_ids = search(query, top_k=top_k)
15
+ hits = []
16
+ for doc_id in doc_ids:
17
+ text = load_document(doc_id)
18
+ snippet = text[:400] + ("..." if len(text) > 400 else "")
19
+ hits.append({"doc_id": doc_id, "snippet": snippet})
20
+ return {"hits": hits}
21
+
22
+
23
+ SCHEMA = {
24
+ "type": "function",
25
+ "function": {
26
+ "name": "search_knowledge",
27
+ "description": (
28
+ "사내 지식 문서(과거 사례 INC, 실패 모드 FMEA, 표준 절차 SOP, 공정 흐름 FLOW)를 "
29
+ "hybrid 검색해 관련 문서의 doc_id와 본문 요약을 반환합니다. "
30
+ "원인 분석·대응 권고에 필요한 도메인 컨텍스트를 가져올 때 사용하세요."
31
+ ),
32
+ "parameters": {
33
+ "type": "object",
34
+ "properties": {
35
+ "query": {
36
+ "type": "string",
37
+ "description": "검색할 키워드/질의문 (예: 'CMP 슬러리 유량 이상')",
38
+ },
39
+ "top_k": {
40
+ "type": "integer",
41
+ "description": "반환할 문서 수 (기본 3)",
42
+ "default": 3,
43
+ },
44
+ },
45
+ "required": ["query"],
46
+ },
47
+ },
48
+ }
agents/tools/process.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """공정 관련 tools - downstream 의존성 + WIP + yield baseline
2
+
3
+ 실 fab에선 MES + Yield Management System에서 조회되는 값
4
+ """
5
+ from data.wip import get_affected_wip
6
+
7
+ # 공정 흐름 (대표 라인: Photo -> Etch -> CMP -> Diffusion -> Implant -> Metal -> Test)
8
+ # 각 entry는 (downstream_stage, typical_delta_pct, severity)
9
+ _DOWNSTREAM = {
10
+ "Photo": [
11
+ ("Etch", 18, "impacted"),
12
+ ("CMP", 5, "minor"),
13
+ ],
14
+ "Etch": [
15
+ ("CMP", 15, "impacted"),
16
+ ("Diffusion", 3, "minor"),
17
+ ],
18
+ "CMP": [
19
+ ("Diffusion", 8, "impacted"),
20
+ ("Implant", 12, "impacted"),
21
+ ("Test", 6, "minor"),
22
+ ],
23
+ "Diffusion": [
24
+ ("Implant", 10, "impacted"),
25
+ ("Metal", 4, "minor"),
26
+ ],
27
+ "Implant": [
28
+ ("Metal", 7, "impacted"),
29
+ ("Test", 5, "minor"),
30
+ ],
31
+ }
32
+
33
+ # 공정별 yield 기준선 (최근 30일 평균, %)
34
+ _YIELD_BASELINE = {
35
+ "Photo": 96.4,
36
+ "Etch": 95.8,
37
+ "CMP": 96.1,
38
+ "Diffusion": 97.2,
39
+ "Implant": 96.7,
40
+ }
41
+
42
+
43
+ def get_downstream_steps(current_stage: str) -> dict:
44
+ """현재 공정 이후로 영향 받는 후공정 목록 반환
45
+
46
+ 각 후공정에 대해 typical delta(%, 영향 강도)와 severity(impacted/minor) 제공
47
+ """
48
+ deps = _DOWNSTREAM.get(current_stage, [])
49
+ return {
50
+ "current_stage": current_stage,
51
+ "downstream": [
52
+ {"stage": s, "typical_delta_pct": d, "severity": sev}
53
+ for s, d, sev in deps
54
+ ],
55
+ }
56
+
57
+
58
+ def query_wip_status(alarm_id: str) -> dict:
59
+ """알람으로 영향 받는 WIP(가공중·대기중) lot 수 + wafer 수 조회"""
60
+ lots = get_affected_wip(alarm_id)
61
+ return {"alarm_id": alarm_id, "wip": lots}
62
+
63
+
64
+ def get_yield_baseline(process: str) -> dict:
65
+ """공정의 최근 30일 yield 기준선(%) 반환
66
+
67
+ 이상 발생 시 baseline 대비 yield_loss 계산에 사용
68
+ """
69
+ baseline = _YIELD_BASELINE.get(process)
70
+ if baseline is None:
71
+ return {"error": f"등록되지 않은 공정: {process}"}
72
+ return {"process": process, "baseline_yield_pct": baseline, "window": "최근 30일"}
73
+
74
+
75
+ SCHEMA_DOWNSTREAM = {
76
+ "type": "function",
77
+ "function": {
78
+ "name": "get_downstream_steps",
79
+ "description": (
80
+ "현재 공정 이후 영향 받는 후공정 목록을 반환합니다. "
81
+ "각 후공정의 일반적 영향 강도(typical_delta_pct)와 severity(impacted/minor)를 포함. "
82
+ "Tier 3 영향 평가에서 downstream_dependencies 산출에 사용하세요."
83
+ ),
84
+ "parameters": {
85
+ "type": "object",
86
+ "properties": {
87
+ "current_stage": {
88
+ "type": "string",
89
+ "description": "현재 공정 (Photo, Etch, CMP, Diffusion, Implant 중)",
90
+ },
91
+ },
92
+ "required": ["current_stage"],
93
+ },
94
+ },
95
+ }
96
+
97
+ SCHEMA_WIP = {
98
+ "type": "function",
99
+ "function": {
100
+ "name": "query_wip_status",
101
+ "description": (
102
+ "알람으로 영향 받는 WIP(가공중·대기중) lot/wafer 수를 MES에서 조회합니다. "
103
+ "Tier 3 impact_lots 필드를 채울 때 사용하세요."
104
+ ),
105
+ "parameters": {
106
+ "type": "object",
107
+ "properties": {
108
+ "alarm_id": {
109
+ "type": "string",
110
+ "description": "알람 ID (A1, A2, A3)",
111
+ },
112
+ },
113
+ "required": ["alarm_id"],
114
+ },
115
+ },
116
+ }
117
+
118
+ SCHEMA_YIELD = {
119
+ "type": "function",
120
+ "function": {
121
+ "name": "get_yield_baseline",
122
+ "description": (
123
+ "공정의 최근 30일 yield 기준선(%)을 조회합니다. "
124
+ "yield_loss를 baseline 대비로 정량화할 때 사용하세요."
125
+ ),
126
+ "parameters": {
127
+ "type": "object",
128
+ "properties": {
129
+ "process": {
130
+ "type": "string",
131
+ "description": "공정 이름 (Photo, Etch, CMP, Diffusion, Implant)",
132
+ },
133
+ },
134
+ "required": ["process"],
135
+ },
136
+ },
137
+ }