fabagent / agents /planner.py
hee_!J
feat(conductor): Plan-and-Execute ํŒจํ„ด - Central Planner + Tier executor (env AGENT_MODE)
3fb690f
Raw
History Blame Contribute Delete
7.51 kB
"""Central Planner Agent (Plan-and-Execute pattern)
์•Œ๋žŒ๊ณผ Tier 1 ๊ฒฐ๊ณผ๋ฅผ ๋ณด๊ณ  ์ „์ฒด ์›Œํฌํ”Œ๋กœ์šฐ plan์„ 1ํšŒ LLM ํ˜ธ์ถœ๋กœ ์‚ฐ์ถœํ•ฉ๋‹ˆ๋‹ค.
๊ฐ Tier executor๋Š” plan๋Œ€๋กœ ๋„๊ตฌ๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๊ณ  LLM 1ํšŒ๋กœ ๊ฒฐ๊ณผ๋ฅผ ํ•ฉ์„ฑํ•ฉ๋‹ˆ๋‹ค.
๊ธฐ์กด autonomous tool-using loop ๋Œ€๋น„:
- LLM ํ˜ธ์ถœ ~70% ๊ฐ์†Œ (๊ฐ tier loop ์ œ๊ฑฐ)
- ์žฌ๊ท€/๋ฌดํ•œ๋ฃจํ”„ ์œ„ํ—˜ ์›์ฒœ ์ฐจ๋‹จ (LLM์ด tool์„ ์ž์œจ ์„ ํƒํ•˜์ง€ ์•Š์Œ)
- ํ†ต์‹  ์˜ค๋ฒ„ํ—ค๋“œ ์ตœ์†Œํ™” (synthesis context๋ฅผ ํ•œ ๋ฒˆ์— ๊ตฌ์„ฑ)
- ๋น„์šฉยทlatency ํฐ ํญ ์ ˆ๊ฐ (gpt-4o-mini๋กœ planning + gpt-5-mini๋กœ synthesis)
์„ค๊ณ„:
- Planner๊ฐ€ ๊ตฌ์ฒด์  ํ•จ์ˆ˜ ํ˜ธ์ถœ์ด ์•„๋‹Œ "ํ•„์š”ํ•œ ์ •๋ณด ์ฑ„๋„" (query/symptom/equipment_id ๋ฆฌ์ŠคํŠธ)์„ ์ง€์ •
- ๊ฐ Tier executor๊ฐ€ ๊ทธ ์ฑ„๋„์„ ๋ฐ›์•„ ๊ฒฐ์ •๋ก ์ ์œผ๋กœ tool ํ˜ธ์ถœ (๋ณ‘๋ ฌ ๊ฐ€๋Šฅ)
- Tier executor๋Š” ๋‹จ 1ํšŒ LLM ํ˜ธ์ถœ๋กœ synthesis (tools ์ธ์ž ์—†์ด)
"""
import json
from langsmith import traceable
from agents.llm import client
from agents.tools.equipment import ALARM_EQUIPMENT
from core.schema import Tier1
PLANNER_MODEL = "gpt-4o-mini"
PLAN_SCHEMA = {
"type": "object",
"properties": {
"tier2": {
"type": "object",
"properties": {
"search_queries": {
"type": "array",
"items": {"type": "string"},
"description": "์‚ฌ๋‚ด ์ง€์‹ ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ (1~3๊ฐœ)",
},
"incident_symptoms": {
"type": "array",
"items": {"type": "string"},
"description": "๊ณผ๊ฑฐ incident DB ์กฐํšŒ์šฉ ์ฆ์ƒ (0~2๊ฐœ)",
},
"equipment_ids": {
"type": "array",
"items": {"type": "string"},
"description": "PM ์ด๋ ฅ ์กฐํšŒ ์žฅ๋น„ ID (0~1๊ฐœ)",
},
},
"required": ["search_queries", "incident_symptoms", "equipment_ids"],
"additionalProperties": False,
},
"tier3": {
"type": "object",
"properties": {
"alarm_id": {"type": "string", "description": "WIP ์กฐํšŒ์šฉ ์•Œ๋žŒ ID"},
"downstream_stages": {
"type": "array",
"items": {"type": "string"},
"description": "ํ›„๊ณต์ • ์˜์กด์„ฑ ์กฐํšŒ (1๊ฐœ)",
},
"yield_processes": {
"type": "array",
"items": {"type": "string"},
"description": "yield baseline ์กฐํšŒ (1~2๊ฐœ)",
},
"equipment_ids": {
"type": "array",
"items": {"type": "string"},
"description": "PM ์ด๋ ฅ ์กฐํšŒ ์žฅ๋น„ ID (0~1๊ฐœ)",
},
},
"required": ["alarm_id", "downstream_stages", "yield_processes", "equipment_ids"],
"additionalProperties": False,
},
"tier4": {
"type": "object",
"properties": {
"search_queries": {
"type": "array",
"items": {"type": "string"},
"description": "SOP ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ (1~3๊ฐœ)",
},
"incident_symptoms": {
"type": "array",
"items": {"type": "string"},
"description": "๊ณผ๊ฑฐ ํ•ด๊ฒฐ์ฑ… ์กฐํšŒ (0~2๊ฐœ)",
},
"equipment_ids": {
"type": "array",
"items": {"type": "string"},
"description": "PM ์œˆ๋„์šฐ ์กฐํšŒ ์žฅ๋น„ ID (1๊ฐœ)",
},
},
"required": ["search_queries", "incident_symptoms", "equipment_ids"],
"additionalProperties": False,
},
"action": {
"type": "string",
"enum": ["proceed_full", "fast_track", "escalate"],
"description": "์›Œํฌํ”Œ๋กœ์šฐ ๋ถ„๊ธฐ (Tier 3 skip ์—ฌ๋ถ€, escalation ํ”Œ๋ž˜๊ทธ)",
},
"severity": {
"type": "string",
"enum": ["low", "medium", "high", "critical"],
},
"reasoning": {"type": "string"},
},
"required": ["tier2", "tier3", "tier4", "action", "severity", "reasoning"],
"additionalProperties": False,
}
SYSTEM_PROMPT = """๋‹น์‹ ์€ ๋ฐ˜๋„์ฒด fab ์ธ์‹œ๋˜ํŠธ ๋Œ€์‘ ์‹œ์Šคํ…œ์˜ Central Planner Agent์ž…๋‹ˆ๋‹ค.
์ด์ƒ ์•Œ๋žŒ๊ณผ Tier 1 ํƒ์ง€ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์•„ ์ „์ฒด ์›Œํฌํ”Œ๋กœ์šฐ(Tier 2 ์›์ธ๋ถ„์„ โ†’ Tier 3 ์˜ํ–ฅํ‰๊ฐ€ โ†’ Tier 4 ๋Œ€์‘๊ถŒ๊ณ )์— ํ•„์š”ํ•œ ์ •๋ณด ์ˆ˜์ง‘ plan์„ ํ•œ ๋ฒˆ์— ์‚ฐ์ถœํ•ฉ๋‹ˆ๋‹ค.
[๊ฐ€์šฉ ์ •๋ณด ์ฑ„๋„ - tier๋ณ„]
Tier 2 (์›์ธ ๋ถ„์„):
- search_queries: ์‚ฌ๋‚ด ์ง€์‹(INC/FMEA/SOP/FLOW) hybrid ๊ฒ€์ƒ‰ (1~3๊ฐœ)
- incident_symptoms: ๊ณผ๊ฑฐ incident DB ๊ตฌ์กฐํ™” ์กฐํšŒ (0~2๊ฐœ)
- equipment_ids: ์žฅ๋น„ PM ์ด๋ ฅ ์กฐํšŒ (0~1๊ฐœ)
Tier 3 (์˜ํ–ฅ ํ‰๊ฐ€):
- alarm_id: WIP ์กฐํšŒ (๋ฐ˜๋“œ์‹œ 1๊ฐœ, ์•Œ๋žŒ ID ๊ทธ๋Œ€๋กœ)
- downstream_stages: ํ›„๊ณต์ • ์˜์กด์„ฑ (1๊ฐœ, ํ˜„์žฌ ๊ณต์ •๋ช… - ์˜ˆ: "Photo", "Etch", "CMP")
- yield_processes: yield baseline ์กฐํšŒ (1~2๊ฐœ)
- equipment_ids: PM ์ด๋ ฅ ์กฐํšŒ (0~1๊ฐœ)
Tier 4 (๋Œ€์‘ ๊ถŒ๊ณ ):
- search_queries: SOPยทํ‘œ์ค€ ์ ˆ์ฐจ ๊ฒ€์ƒ‰ (1~3๊ฐœ)
- incident_symptoms: ๊ณผ๊ฑฐ ํ•ด๊ฒฐ์ฑ… ์กฐํšŒ (0~2๊ฐœ)
- equipment_ids: PM ์œˆ๋„์šฐ ์กฐํšŒ (1๊ฐœ, immediate ์กฐ์น˜์˜ ์‹คํ–‰ ๊ฐ€๋Šฅ ์‹œ์ )
[routing ๊ฒฐ์ •]
- action:
- proceed_full (๋Œ€๋ถ€๋ถ„, ํ‘œ์ค€ Tier 2 โ†’ 3 โ†’ 4)
- fast_track (๋‹จ์ผ ์›์ธ 80%+ ์šฐ์„ธ ์‹œ - Tier 3 LLM skip)
- escalate (๊ณ ์œ„ํ—˜ยท๋‹ค๋Œ€์ˆ˜ ์˜ํ–ฅ - ์ •์ƒ ์ง„ํ–‰ + human review ํ”Œ๋ž˜๊ทธ)
- severity: low / medium / high / critical
[์ค‘์š” ์›์น™]
- ์•Œ๋žŒ๊ณผ Tier 1์„ ๋ณด๊ณ  ๊ฐ€์žฅ ์ •๋ณด๊ฐ€์น˜ ํฐ ์ฟผ๋ฆฌ/์กฐํšŒ๋งŒ ์„ ๋ณ„ (๋ถˆํ•„์š”ํ•œ ํ˜ธ์ถœ ์ œ๊ฑฐ)
- search_queries๋Š” ๋„๋ฉ”์ธ ์šฉ์–ด๋ฅผ ํ’๋ถ€ํ•˜๊ฒŒ (์˜ˆ: "Photo CD ์‚ฐํฌ ๋ Œ์ฆˆ ์˜ค์—ผ ํ—ค์ด์ฆˆ")
- Tier 2์™€ Tier 4์˜ search_queries๋Š” ๋ชฉ์ ์ด ๋‹ค๋ฆ„:
- Tier 2: ์›์ธ ํ›„๋ณด ํƒ์ƒ‰์šฉ
- Tier 4: SOPยท์กฐ์น˜ ์ ˆ์ฐจ ํƒ์ƒ‰์šฉ
- reasoning์€ ํ•œ ๋ฌธ์žฅ์œผ๋กœ plan์˜ ํ•ต์‹ฌ ์˜๋„ ๋ช…์‹œ"""
def _build_user_prompt(alarm: dict, tier1: Tier1) -> str:
sensors = ", ".join(f["name"] for f in tier1["features"])
equipment_id = ALARM_EQUIPMENT.get(alarm["id"], "(๋ฏธ๋งคํ•‘)")
process = alarm["title"].split()[0]
return f"""## ์ด์ƒ ์•Œ๋žŒ
- ๊ณต์ •: {alarm['title']} (current_stage: {process})
- lot: {alarm['lot_id']}
- ์ด์ƒ ํ”ผ์ฒ˜: {alarm.get('feature')} {alarm.get('feature_arrow') or ''}
- ์•Œ๋žŒ ID: {alarm['id']}
- ์ถ”์ • ์žฅ๋น„ ID: {equipment_id}
## Tier 1 ์ด์ƒ ํƒ์ง€ ๊ฒฐ๊ณผ
- ์ด์ƒ ์ ์ˆ˜: {tier1['score']}
- ๊ธฐ์—ฌ ์„ผ์„œ(Top): {sensors}
์œ„ ์•Œ๋žŒ์˜ ์ „์ฒด ์›Œํฌํ”Œ๋กœ์šฐ(Tier 2โ†’3โ†’4) plan์„ ์‚ฐ์ถœํ•˜์„ธ์š”."""
@traceable(name="Planner_Agent", run_type="chain")
def plan_workflow(alarm: dict, tier1: Tier1) -> dict:
"""์•Œ๋žŒ + Tier 1์„ ๋ณด๊ณ  ์ „์ฒด ์›Œํฌํ”Œ๋กœ์šฐ plan์„ ํ•œ ๋ฒˆ์— ์‚ฐ์ถœ
๋ฐ˜ํ™˜: PLAN_SCHEMA ํ˜•์‹ dict (tier2/tier3/tier4 + action/severity/reasoning)
"""
resp = client().chat.completions.create(
model=PLANNER_MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": _build_user_prompt(alarm, tier1)},
],
response_format={
"type": "json_schema",
"json_schema": {"name": "plan", "schema": PLAN_SCHEMA, "strict": True},
},
)
return json.loads(resp.choices[0].message.content)