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 +69 -0
- agents/tools/equipment.py +123 -0
- agents/tools/incident.py +107 -0
- agents/tools/knowledge.py +48 -0
- agents/tools/process.py +137 -0
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 |
+
}
|