hee_!J
feat(conductor): Plan-and-Execute ํจํด - Central Planner + Tier executor (env AGENT_MODE)
3fb690f | """4-Tier ๋ฉํฐ์์ด์ ํธ ์ค์ผ์คํธ๋ ์ดํฐ (LangGraph ๊ธฐ๋ฐ) | |
| ๋ ๊ฐ์ง ์คํ ๋ชจ๋ ์ง์ (ํ๊ฒฝ๋ณ์ AGENT_MODE): | |
| - **conductor** (๊ธฐ๋ณธ): Central Planner Agent๊ฐ ์ ์ฒด plan 1ํ ์ฐ์ถ | |
| ๊ฐ Tier executor๊ฐ plan๋๋ก tool ํธ์ถ + LLM 1ํ synthesis | |
| โ LLM ํธ์ถ ~70% ๊ฐ์, latency ํฐ ํญ ๋จ์ถ | |
| - **autonomous**: ๊ฐ Tier๊ฐ tool-using agent loop๋ก ์์จ ์คํ | |
| โ ์ ์์ฑ ๋์ผ๋ LLM ํธ์ถ ๋ง์ | |
| [๊ทธ๋ํ - conductor ๋ชจ๋] | |
| START โ detect โ (severity gate) โ planner โ (action ๋ถ๊ธฐ) | |
| โ | |
| +โ cause โ impact โ response โ END (proceed_full) | |
| +โ cause โ fast_impact โ response โ END (fast_track) | |
| +โ cause โ impact โ response_escalate โ END (escalate, T4์ ๐จ prepend) | |
| [๊ทธ๋ํ - autonomous ๋ชจ๋] | |
| ๊ธฐ์กด ๊ตฌ์กฐ (severity gate + cause_retry + supervisor ๋ถ๊ธฐ) | |
| ์๋ dict์ data.demo.DEFAULT_ALARMS์์ ID๋ก ์กฐํ. | |
| @traceable (LangSmith) + wrap_openai ๋ก LLMยทtool ํธ์ถ ์๋ ํธ๋ ์ด์ค. | |
| """ | |
| import os | |
| from functools import lru_cache | |
| from typing import TypedDict | |
| from langgraph.graph import END, START, StateGraph | |
| from langsmith import traceable | |
| from agents.cause import run_cause | |
| from agents.detection import run_detection | |
| from agents.impact import run_impact | |
| from agents.planner import plan_workflow | |
| from agents.response import run_response | |
| from agents.supervisor import run_supervisor | |
| from core.schema import Tier1, Tier2, Tier3, Tier4, TierData | |
| from data.demo import DEFAULT_ALARMS | |
| from data.wip import get_affected_wip | |
| SEVERITY_THRESHOLD = 0.30 | |
| CAUSE_CONFIDENCE_THRESHOLD = 40 | |
| def _agent_mode() -> str: | |
| return os.getenv("AGENT_MODE", "conductor").lower() | |
| class _GraphState(TypedDict, total=False): | |
| alarm: dict | |
| tier1: Tier1 | |
| tier2: Tier2 | |
| tier3: Tier3 | |
| tier4: Tier4 | |
| skipped: bool | |
| cause_retried: bool | |
| supervisor_decision: dict | |
| plan: dict # conductor ๋ชจ๋์ Planner ๊ฒฐ๊ณผ | |
| def _node_detect(state: _GraphState) -> dict: | |
| return {"tier1": run_detection(state["alarm"])} | |
| def _route_after_detect(state: _GraphState) -> str: | |
| return "go" if state["tier1"]["score"] >= SEVERITY_THRESHOLD else "noise" | |
| def _node_noise(state: _GraphState) -> dict: | |
| return { | |
| "skipped": True, | |
| "tier2": {"causes": []}, | |
| "tier3": {"yield_loss": 0.0, "dependencies": [], "impact_lots": []}, | |
| "tier4": {"immediate": [], "longterm": [], "refs": []}, | |
| } | |
| # ==================== Conductor ๋ชจ๋ ๋ ธ๋ ==================== | |
| def _node_planner(state: _GraphState) -> dict: | |
| return {"plan": plan_workflow(state["alarm"], state["tier1"])} | |
| def _route_after_planner(state: _GraphState) -> str: | |
| action = state["plan"].get("action", "proceed_full") | |
| if action == "fast_track": | |
| return "fast_track" | |
| return "proceed" # proceed_full + escalate ๋ชจ๋ ํ์ค ๊ฒฝ๋ก | |
| def _node_cause_conductor(state: _GraphState) -> dict: | |
| return {"tier2": run_cause(state["alarm"], state["tier1"], plan=state["plan"])} | |
| def _node_impact_conductor(state: _GraphState) -> dict: | |
| return {"tier3": run_impact(state["alarm"], state["tier1"], state["tier2"], plan=state["plan"])} | |
| def _node_fast_impact(state: _GraphState) -> dict: | |
| """fast_track: deterministic ๊ฒฝ๋ ์ฒ๋ฆฌ (LLM ํธ์ถ ์์)""" | |
| alarm = state["alarm"] | |
| tier1 = state["tier1"] | |
| return { | |
| "tier3": { | |
| "yield_loss": round(float(tier1["score"]) * 3.0, 1), | |
| "dependencies": [{ | |
| "stage": alarm["title"].split()[0], | |
| "delta": f"+{tier1['score']}", | |
| "tag": "ํ์ฌ", | |
| "kind": "current", | |
| }], | |
| "impact_lots": get_affected_wip(alarm["id"]), | |
| } | |
| } | |
| def _node_response_conductor(state: _GraphState) -> dict: | |
| tier4 = run_response(state["alarm"], state["tier1"], state["tier2"], state["tier3"], plan=state["plan"]) | |
| if state["plan"].get("action") == "escalate" and tier4.get("immediate"): | |
| tier4["immediate"][0]["text"] = "๐จ [HUMAN REVIEW ์๊ตฌ] " + tier4["immediate"][0]["text"] | |
| return {"tier4": tier4} | |
| # ==================== Autonomous ๋ชจ๋ ๋ ธ๋ (๊ธฐ์กด ๋ณด์กด) ==================== | |
| def _node_cause_autonomous(state: _GraphState) -> dict: | |
| return {"tier2": run_cause(state["alarm"], state["tier1"])} | |
| def _route_after_cause_autonomous(state: _GraphState) -> str: | |
| if state.get("cause_retried"): | |
| return "supervisor" | |
| causes = state["tier2"]["causes"] | |
| max_pct = max((c.get("pct", 0) for c in causes), default=0) | |
| return "cause_retry" if max_pct < CAUSE_CONFIDENCE_THRESHOLD else "supervisor" | |
| def _node_cause_retry(state: _GraphState) -> dict: | |
| return { | |
| "tier2": run_cause(state["alarm"], state["tier1"], retry_hint=True), | |
| "cause_retried": True, | |
| } | |
| def _node_supervisor(state: _GraphState) -> dict: | |
| return {"supervisor_decision": run_supervisor(state["alarm"], state["tier1"], state["tier2"])} | |
| def _route_after_supervisor(state: _GraphState) -> str: | |
| return "fast_track" if state["supervisor_decision"]["action"] == "fast_track" else "proceed" | |
| def _node_impact_autonomous(state: _GraphState) -> dict: | |
| return {"tier3": run_impact(state["alarm"], state["tier1"], state["tier2"])} | |
| def _node_response_autonomous(state: _GraphState) -> dict: | |
| tier4 = run_response(state["alarm"], state["tier1"], state["tier2"], state["tier3"]) | |
| decision = state.get("supervisor_decision", {}) | |
| if decision.get("action") == "escalate" and tier4.get("immediate"): | |
| tier4["immediate"][0]["text"] = "๐จ [HUMAN REVIEW ์๊ตฌ] " + tier4["immediate"][0]["text"] | |
| return {"tier4": tier4} | |
| # ==================== Graph ๋น๋ ==================== | |
| def build_graph(mode: str = "conductor"): | |
| g = StateGraph(_GraphState) | |
| g.add_node("detect", _node_detect) | |
| g.add_node("noise", _node_noise) | |
| g.add_edge(START, "detect") | |
| g.add_edge("noise", END) | |
| if mode == "conductor": | |
| g.add_node("planner", _node_planner) | |
| g.add_node("cause", _node_cause_conductor) | |
| g.add_node("impact", _node_impact_conductor) | |
| g.add_node("fast_impact", _node_fast_impact) | |
| g.add_node("response", _node_response_conductor) | |
| g.add_conditional_edges("detect", _route_after_detect, {"go": "planner", "noise": "noise"}) | |
| g.add_edge("planner", "cause") | |
| g.add_conditional_edges("cause", _route_after_planner, { | |
| "proceed": "impact", | |
| "fast_track": "fast_impact", | |
| }) | |
| g.add_edge("impact", "response") | |
| g.add_edge("fast_impact", "response") | |
| g.add_edge("response", END) | |
| else: | |
| # autonomous (๊ธฐ์กด ๊ตฌ์กฐ) | |
| g.add_node("cause", _node_cause_autonomous) | |
| g.add_node("cause_retry", _node_cause_retry) | |
| g.add_node("supervisor", _node_supervisor) | |
| g.add_node("impact", _node_impact_autonomous) | |
| g.add_node("fast_impact", _node_fast_impact) | |
| g.add_node("response", _node_response_autonomous) | |
| g.add_conditional_edges("detect", _route_after_detect, {"go": "cause", "noise": "noise"}) | |
| g.add_conditional_edges("cause", _route_after_cause_autonomous, { | |
| "supervisor": "supervisor", | |
| "cause_retry": "cause_retry", | |
| }) | |
| g.add_edge("cause_retry", "supervisor") | |
| g.add_conditional_edges("supervisor", _route_after_supervisor, { | |
| "proceed": "impact", | |
| "fast_track": "fast_impact", | |
| }) | |
| g.add_edge("impact", "response") | |
| g.add_edge("fast_impact", "response") | |
| g.add_edge("response", END) | |
| return g.compile() | |
| def _find_alarm(alarm_id: str) -> dict: | |
| for a in DEFAULT_ALARMS: | |
| if a["id"] == alarm_id: | |
| return a | |
| raise ValueError(f"์๋ ID๋ฅผ ์ฐพ์ ์ ์์: {alarm_id}") | |
| def run_orchestrator(alarm_id: str) -> TierData: | |
| alarm = _find_alarm(alarm_id) | |
| graph = build_graph(_agent_mode()) | |
| final = graph.invoke({"alarm": alarm}) | |
| return { | |
| "tier1": final["tier1"], | |
| "tier2": final["tier2"], | |
| "tier3": final["tier3"], | |
| "tier4": final["tier4"], | |
| } | |