hee_!J
feat(conductor): Plan-and-Execute ํจํด - Central Planner + Tier executor (env AGENT_MODE)
3fb690f | """Tier 3 ๊ณต์ ๊ฐ ์ํฅ ํ๊ฐ ์์ด์ ํธ | |
| ๋ ๋ชจ๋ ์ง์: | |
| 1. Autonomous (plan=None): LLM์ด tool์ ์์จ ํธ์ถํ๋ agent loop | |
| 2. Conductor (plan ์ ๊ณต): Planner๊ฐ ์ง์ ํ tool ํธ์ถ + ๋จ์ผ LLM synthesis | |
| ๊ฐ์ฉ ๋๊ตฌ: query_wip_status, get_downstream_steps, get_yield_baseline, get_pm_history | |
| """ | |
| import json | |
| from langsmith import traceable | |
| from agents.cause import _assistant_msg_dict | |
| from agents.llm import SUBAGENT_MODEL, client | |
| from agents.tools import TOOLS_IMPACT, dispatch_tool | |
| from agents.tools.equipment import ALARM_EQUIPMENT | |
| from core.schema import Tier1, Tier2, Tier3 | |
| from data.wip import get_affected_wip | |
| MAX_TOOL_ITERATIONS = 4 | |
| LLM_PART_SCHEMA = { | |
| "type": "object", | |
| "properties": { | |
| "yield_loss": {"type": "number"}, | |
| "downstream_dependencies": { | |
| "type": "array", | |
| "items": { | |
| "type": "object", | |
| "properties": { | |
| "stage": {"type": "string"}, | |
| "delta": {"type": "string"}, | |
| "tag": {"type": "string"}, | |
| "kind": {"type": "string", "enum": ["impacted", "minor"]}, | |
| }, | |
| "required": ["stage", "delta", "tag", "kind"], | |
| "additionalProperties": False, | |
| }, | |
| }, | |
| }, | |
| "required": ["yield_loss", "downstream_dependencies"], | |
| "additionalProperties": False, | |
| } | |
| SYSTEM_PROMPT = """๋น์ ์ ๋ฐ๋์ฒด ๊ณต์ ์ํฅ ํ๊ฐ ์ ๋ฌธ๊ฐ์ ๋๋ค. | |
| ์ด์์ด ๋ฐ์ํ ๊ณต์ ์์ ์ถ๋ฐํด downstream ์ํฅ์ ์ ๋ ํ๊ฐํฉ๋๋ค. | |
| [๊ฐ์ฉ ๋๊ตฌ] | |
| - query_wip_status(alarm_id): ์ํฅ ๋ฐ๋ WIP(๊ฐ๊ณต์คยท๋๊ธฐ์ค) lot/wafer ์ | |
| - get_downstream_steps(current_stage): ํ๊ณต์ ์์กด์ฑ (typical_delta_pctยทseverity ํฌํจ) | |
| - get_yield_baseline(process): ๊ณต์ ์ต๊ทผ 30์ผ yield ๊ธฐ์ค์ - baseline ๋๋น๋ก yield_loss ์ ๋ํ | |
| - get_pm_history(equipment_id): ์ฅ๋น PM ์ด๋ ฅ - PM ๋๋ฝ์ด ์ํฅ์ ์ฆํญ์ํค๋ ์์ธ์ธ์ง ํ๋จ | |
| [์ ๋ต] | |
| - ๋๊ตฌ๋ฅผ ์์จ ํธ์ถํด ์ ๋ ๊ทผ๊ฑฐ๋ฅผ ๋ชจ์ผ์ธ์ | |
| - downstream ํ๊ณต์ ์ ์๋ ค๋ฉด get_downstream_steps๋ฅผ ํธ์ถํด fab ํ๋ฆ ํ์ | |
| - yield_loss๋ Tier 2 ์์ธ์ ๊ธฐ์ฌ๋ยท๊ณผ๊ฑฐ incident ํ๋ณต๋ฅ ยทbaseline์ ์ข ํฉํด ์ถ์ | |
| - ์ถฉ๋ถํ ์ ๋ณด๊ฐ ๋ชจ์ด๋ฉด ์ข ๋ฃํด ์ต์ข ๊ตฌ์กฐํ ์ถ๋ ฅ์ผ๋ก ๋์ด๊ฐ์ธ์ | |
| [์ต์ข ์ฐ์ถ๋ฌผ] | |
| - yield_loss: ๋ณธ ์ด์์ผ๋ก ์ธํ ์์ ์์จ ์์ค(%p, ์์ ํ ์๋ฆฌ) | |
| - downstream_dependencies: ์ํฅ ๋ฐ๋ ํ๊ณต์ (current ์ ์ธ) - stage / delta / tag / kind""" | |
| def _stage_from_alarm(alarm: dict) -> str: | |
| return alarm["title"].split()[0] | |
| def _initial_user_prompt(alarm: dict, tier1: Tier1, tier2: Tier2) -> str: | |
| cause_lines = "\n".join( | |
| f"- {c['name']} ({c['pct']}%): {c['evidence'][:120]}" for c in tier2["causes"] | |
| ) | |
| equipment_id = ALARM_EQUIPMENT.get(alarm["id"], "(๋ฏธ๋งคํ)") | |
| current_stage = _stage_from_alarm(alarm) | |
| return f"""## ์ด์ ์๋ | |
| - ๊ณต์ : {alarm['title']} (current_stage: {current_stage}) | |
| - lot: {alarm['lot_id']} | |
| - ์๋ ID: {alarm['id']} | |
| - ์ถ์ ์ฅ๋น ID: {equipment_id} | |
| ## Tier 1 ์ด์ ํ์ง | |
| - ์ด์ ์ ์: {tier1['score']} | |
| ## Tier 2 ์์ธ ๋ถ์ (๊ธฐ์ฌ๋ ์) | |
| {cause_lines} | |
| ์ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก yield_loss์ downstream_dependencies๋ฅผ ์ฐ์ถํด ์ฃผ์ธ์. | |
| WIPยทdownstreamยทyieldยทPM ์ปจํ ์คํธ๋ ๋๊ตฌ๋ฅผ ํธ์ถํด ์์จ์ ์ผ๋ก ์์งํ์ธ์.""" | |
| CONDUCTOR_SYSTEM_PROMPT = """๋น์ ์ ๋ฐ๋์ฒด ๊ณต์ ์ํฅ ํ๊ฐ ์ ๋ฌธ๊ฐ์ ๋๋ค. | |
| Central Planner๊ฐ ์ด๋ฏธ ํ์ํ ์ ๋ณด(WIPยทdownstreamยทyieldยทPM)๋ฅผ ๋ชจ๋ ์์งํด [์์ง๋ ์ปจํ ์คํธ]์ ์ ๋ฆฌํด ๋์์ต๋๋ค. | |
| ๋น์ ์ ์ถ๊ฐ ๋๊ตฌ ํธ์ถ ์์ด ๊ทธ ์ปจํ ์คํธ๋ง ์ฌ์ฉํด ์ฐ์ถํ์ธ์. | |
| [์ต์ข ์ฐ์ถ๋ฌผ] | |
| - yield_loss: ๋ณธ ์ด์์ผ๋ก ์ธํ ์์ ์์จ ์์ค(%p, ์์ ํ ์๋ฆฌ) | |
| - downstream_dependencies: ์ํฅ ๋ฐ๋ ํ๊ณต์ (current ์ ์ธ) - stage / delta / tag / kind""" | |
| def _execute_tier3_plan(plan_tier3: dict, trace_calls: list) -> str: | |
| """Planner๊ฐ ์ง์ ํ tool๋ค์ ์ง์ ํธ์ถํ๊ณ ์ปจํ ์คํธ๋ก ๋ณํ""" | |
| blocks = [] | |
| if plan_tier3.get("alarm_id"): | |
| r = dispatch_tool("query_wip_status", {"alarm_id": plan_tier3["alarm_id"]}) | |
| trace_calls.append({"name": "query_wip_status", "args": {"alarm_id": plan_tier3["alarm_id"]}}) | |
| blocks.append(f"[query_wip_status: {plan_tier3['alarm_id']!r}]\n{r}") | |
| for stage in plan_tier3.get("downstream_stages", []): | |
| r = dispatch_tool("get_downstream_steps", {"current_stage": stage}) | |
| trace_calls.append({"name": "get_downstream_steps", "args": {"current_stage": stage}}) | |
| blocks.append(f"[get_downstream_steps: {stage!r}]\n{r}") | |
| for proc in plan_tier3.get("yield_processes", []): | |
| r = dispatch_tool("get_yield_baseline", {"process": proc}) | |
| trace_calls.append({"name": "get_yield_baseline", "args": {"process": proc}}) | |
| blocks.append(f"[get_yield_baseline: {proc!r}]\n{r}") | |
| for eq_id in plan_tier3.get("equipment_ids", []): | |
| r = dispatch_tool("get_pm_history", {"equipment_id": eq_id}) | |
| trace_calls.append({"name": "get_pm_history", "args": {"equipment_id": eq_id}}) | |
| blocks.append(f"[get_pm_history: {eq_id!r}]\n{r}") | |
| return "\n\n".join(blocks) if blocks else "(planner๊ฐ ์ ๋ณด ์์ง ์ง์ ์์)" | |
| def _run_impact_conductor( | |
| alarm: dict, tier1: Tier1, tier2: Tier2, plan: dict, trace: dict | None | |
| ) -> Tier3: | |
| tool_log: list[dict] = [] | |
| knowledge = _execute_tier3_plan(plan.get("tier3", {}), tool_log) | |
| cause_lines = "\n".join( | |
| f"- {c['name']} ({c['pct']}%): {c['evidence'][:120]}" for c in tier2["causes"] | |
| ) | |
| current_stage = _stage_from_alarm(alarm) | |
| user_prompt = f"""## ์ด์ ์๋ | |
| - ๊ณต์ : {alarm['title']} (current_stage: {current_stage}) | |
| ## Tier 1: ์ด์ ์ ์ {tier1['score']} | |
| ## Tier 2 ์์ธ ๋ถ์ | |
| {cause_lines} | |
| ## ์์ง๋ ์ปจํ ์คํธ (Planner ์ง์ ) | |
| {knowledge} | |
| ์ ์ปจํ ์คํธ๋ง ์ฌ์ฉํด yield_loss์ downstream_dependencies๋ฅผ ์ฐ์ถํ์ธ์.""" | |
| resp = client().chat.completions.create( | |
| model=SUBAGENT_MODEL, | |
| messages=[ | |
| {"role": "system", "content": CONDUCTOR_SYSTEM_PROMPT}, | |
| {"role": "user", "content": user_prompt}, | |
| ], | |
| response_format={ | |
| "type": "json_schema", | |
| "json_schema": {"name": "tier3_part", "schema": LLM_PART_SCHEMA, "strict": True}, | |
| }, | |
| ) | |
| llm_out = json.loads(resp.choices[0].message.content) | |
| current_dep = { | |
| "stage": current_stage, | |
| "delta": f"+{tier1['score']}", | |
| "tag": "ํ์ฌ", | |
| "kind": "current", | |
| } | |
| if trace is not None: | |
| trace["tool_calls"] = tool_log | |
| trace["iterations"] = 0 | |
| trace["llm_calls"] = 1 | |
| trace["mode"] = "conductor" | |
| return { | |
| "yield_loss": round(float(llm_out["yield_loss"]), 1), | |
| "dependencies": [current_dep] + llm_out["downstream_dependencies"], | |
| "impact_lots": get_affected_wip(alarm["id"]), | |
| } | |
| def run_impact( | |
| alarm: dict, | |
| tier1: Tier1, | |
| tier2: Tier2, | |
| trace: dict | None = None, | |
| plan: dict | None = None, | |
| ) -> Tier3: | |
| """Tier 3 ์ํฅ ํ๊ฐ. plan ์ ๊ณต ์ conductor ๋ชจ๋(LLM 1ํ), ์๋๋ฉด autonomous loop.""" | |
| if plan is not None: | |
| return _run_impact_conductor(alarm, tier1, tier2, plan, trace) | |
| messages = [ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": _initial_user_prompt(alarm, tier1, tier2)}, | |
| ] | |
| tool_call_log: list[dict] = [] | |
| iterations = 0 | |
| for iterations in range(1, MAX_TOOL_ITERATIONS + 1): | |
| resp = client().chat.completions.create( | |
| model=SUBAGENT_MODEL, | |
| messages=messages, | |
| tools=TOOLS_IMPACT, | |
| tool_choice="auto", | |
| ) | |
| msg = resp.choices[0].message | |
| messages.append(_assistant_msg_dict(msg)) | |
| if not msg.tool_calls: | |
| break | |
| for tc in msg.tool_calls: | |
| args = json.loads(tc.function.arguments or "{}") | |
| result = dispatch_tool(tc.function.name, args) | |
| tool_call_log.append({"name": tc.function.name, "args": args}) | |
| messages.append({"role": "tool", "tool_call_id": tc.id, "content": result}) | |
| messages.append({ | |
| "role": "user", | |
| "content": "์์งํ ์ ๋ณด๋ฅผ ์ข ํฉํด yield_loss์ downstream_dependencies๋ฅผ JSON ์คํค๋ง์ ๋ง์ถฐ ์ถ๋ ฅํด ์ฃผ์ธ์.", | |
| }) | |
| final = client().chat.completions.create( | |
| model=SUBAGENT_MODEL, | |
| messages=messages, | |
| response_format={ | |
| "type": "json_schema", | |
| "json_schema": {"name": "tier3_part", "schema": LLM_PART_SCHEMA, "strict": True}, | |
| }, | |
| ) | |
| llm_out = json.loads(final.choices[0].message.content) | |
| # current ํญ๋ชฉ์ ๊ฒฐ์ ๋ก ์ (Tier 1 score ๊ธฐ๋ฐ) | |
| current_dep = { | |
| "stage": _stage_from_alarm(alarm), | |
| "delta": f"+{tier1['score']}", | |
| "tag": "ํ์ฌ", | |
| "kind": "current", | |
| } | |
| if trace is not None: | |
| trace["tool_calls"] = tool_call_log | |
| trace["iterations"] = iterations | |
| trace["llm_calls"] = iterations + 1 | |
| trace["mode"] = "autonomous" | |
| return { | |
| "yield_loss": round(float(llm_out["yield_loss"]), 1), | |
| "dependencies": [current_dep] + llm_out["downstream_dependencies"], | |
| "impact_lots": get_affected_wip(alarm["id"]), | |
| } | |