hee_!J
feat(conductor): Plan-and-Execute ํจํด - Central Planner + Tier executor (env AGENT_MODE)
3fb690f | """Tier 2 ์์ธ ๋ถ์ ์์ด์ ํธ | |
| ๋ ๋ชจ๋ ์ง์: | |
| 1. **Autonomous (๊ธฐ๋ณธ, plan=None)**: LLM์ด tool์ ์์จ ํธ์ถํ๋ agent loop | |
| - 2~3 iteration + synthesis = LLM ํธ์ถ 3~4ํ | |
| 2. **Conductor (plan ์ ๊ณต)**: Planner๊ฐ ์ง์ ํ tool ํธ์ถ + ๋จ์ผ LLM synthesis | |
| - Tool ์ง์ ์คํ + LLM ํธ์ถ 1ํ. ์ฌ๊ทยทloop ์์ | |
| ๊ฐ์ฉ ๋๊ตฌ: search_knowledge, lookup_incident_history, get_pm_history | |
| """ | |
| import json | |
| from langsmith import traceable | |
| from agents.llm import SUBAGENT_MODEL, client | |
| from agents.tools import TOOLS_CAUSE, dispatch_tool | |
| from agents.tools.equipment import ALARM_EQUIPMENT | |
| from core.schema import Tier1, Tier2 | |
| MAX_TOOL_ITERATIONS = 4 | |
| TIER2_SCHEMA = { | |
| "type": "object", | |
| "properties": { | |
| "causes": { | |
| "type": "array", | |
| "items": { | |
| "type": "object", | |
| "properties": { | |
| "name": {"type": "string"}, | |
| "pct": {"type": "integer"}, | |
| "evidence": {"type": "string"}, | |
| "citations": {"type": "array", "items": {"type": "string"}}, | |
| }, | |
| "required": ["name", "pct", "evidence", "citations"], | |
| "additionalProperties": False, | |
| }, | |
| } | |
| }, | |
| "required": ["causes"], | |
| "additionalProperties": False, | |
| } | |
| SYSTEM_PROMPT = """๋น์ ์ ๋ฐ๋์ฒด ๊ณต์ ์์ธ ๋ถ์ ์ ๋ฌธ๊ฐ์ ๋๋ค. | |
| ์ด์ ์๋๊ณผ Tier 1 ํ์ง ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ ๊ฐ์ฅ ๊ฐ๋ฅ์ฑ ๋์ ์์ธ 2~3๊ฐ๋ฅผ ์ถ์ ํฉ๋๋ค. | |
| [๊ฐ์ฉ ๋๊ตฌ] | |
| - search_knowledge(query): ์ฌ๋ด ์ง์ ๋ฌธ์(INC/FMEA/SOP/FLOW) hybrid ๊ฒ์ | |
| - lookup_incident_history(symptom): ๊ณผ๊ฑฐ incident ๊ตฌ์กฐํ ์กฐํ (์์ธยทํด๊ฒฐ์ฑ ยทyield ํ๋ณต๋ฅ ) | |
| - get_pm_history(equipment_id): ์ฅ๋น PM ์ด๋ ฅ (๋ง์ง๋ง PM ๊ฒฝ๊ณผ์ผ, overdue ์ฌ๋ถ) | |
| [์ ๋ต] | |
| - ๋๊ตฌ๋ฅผ ์์จ์ ์ผ๋ก ์ ํยทํธ์ถํด ์ถฉ๋ถํ ๊ทผ๊ฑฐ๋ฅผ ๋ชจ์ผ์ธ์ (๋ฐ๋ณต ํธ์ถ ํ์ฉ) | |
| - ๋์ผํ ๋๊ตฌ๋ฅผ ๋ฐ๋ณต ํธ์ถํ์ง ๋ง๊ณ , ํ์ํ ์ ๋ณด๊ฐ ๋ค ๋ชจ์ด๋ฉด ํธ์ถ์ ๋ฉ์ถ์ธ์ | |
| - ๋ชจ์ธ ์ ๋ณด๊ฐ ์ถฉ๋ถํ๋ฉด ์์ฐ์ด๋ก ๋ตํ์ง ๋ง๊ณ ๊ณง๋ฐ๋ก ์ข ๋ฃํด ์ต์ข ๊ตฌ์กฐํ ์ถ๋ ฅ์ผ๋ก ๋์ด๊ฐ์ธ์ | |
| [์ต์ข ์ฐ์ถ๋ฌผ (synthesis ๋จ๊ณ์์ JSON์ผ๋ก)] | |
| - causes: 2~3๊ฐ. ๊ฐ ์์ธ์ name / pct(๊ธฐ์ฌ๋ %) / evidence(๊ตฌ์ฒด์ ๊ทผ๊ฑฐ) / citations(๋ฌธ์ ID ๋๋ incident ID) | |
| - pct ํฉ์ 100์ ๊ฐ๊น์์ผ ํจ, ๊ธฐ์ฌ๋ ๋์ ์์ธ๋ถํฐ ์ ๋ ฌ | |
| - ๋๊ตฌ๋ก ์ป์ง ๋ชปํ ์ ๋ณด๋ ์ธ์ฉํ์ง ๋ง์ธ์""" | |
| def _initial_user_prompt(alarm: dict, tier1: Tier1) -> str: | |
| sensors = ", ".join(f["name"] for f in tier1["features"]) | |
| equipment_id = ALARM_EQUIPMENT.get(alarm["id"], "(๋ฏธ๋งคํ)") | |
| return f"""## ์ด์ ์๋ | |
| - ๊ณต์ : {alarm['title']} | |
| - lot: {alarm['lot_id']} | |
| - ์ด์ ํผ์ฒ: {alarm.get('feature')} {alarm.get('feature_arrow') or ''} | |
| - ์๋ ID: {alarm['id']} | |
| - ์ถ์ ์ฅ๋น ID: {equipment_id} | |
| ## Tier 1 ์ด์ ํ์ง ๊ฒฐ๊ณผ | |
| - ์ด์ ์ ์: {tier1['score']} | |
| - ๊ธฐ์ฌ ์ผ์(Top): {sensors} | |
| ์ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ์์ธ์ ๋ถ์ํด ์ฃผ์ธ์. | |
| ํ์ํ ์ปจํ ์คํธ๋ ๊ฐ์ฉ ๋๊ตฌ๋ฅผ ํธ์ถํด ์์จ์ ์ผ๋ก ์์งํ์ธ์.""" | |
| def _assistant_msg_dict(msg) -> dict: | |
| """OpenAI assistant ์๋ต์ messages ๋ฐฐ์ด์ ๋ค์ ๋ฃ์ dict๋ก ๋ณํ""" | |
| out: dict = {"role": "assistant", "content": msg.content} | |
| if msg.tool_calls: | |
| out["tool_calls"] = [ | |
| { | |
| "id": tc.id, | |
| "type": "function", | |
| "function": {"name": tc.function.name, "arguments": tc.function.arguments}, | |
| } | |
| for tc in msg.tool_calls | |
| ] | |
| return out | |
| CONDUCTOR_SYSTEM_PROMPT = """๋น์ ์ ๋ฐ๋์ฒด ๊ณต์ ์์ธ ๋ถ์ ์ ๋ฌธ๊ฐ์ ๋๋ค. | |
| Central Planner๊ฐ ์ด๋ฏธ ํ์ํ ์ ๋ณด๋ฅผ ๋ชจ๋ ์์งํด [์์ง๋ ์ปจํ ์คํธ]์ ์ ๋ฆฌํด ๋์์ต๋๋ค. | |
| ๋น์ ์ ์ถ๊ฐ ๋๊ตฌ ํธ์ถ ์์ด ๊ทธ ์ปจํ ์คํธ๋ง ์ฌ์ฉํด ์์ธ 2~3๊ฐ๋ฅผ ์ฐ์ถํ์ธ์. | |
| [์ต์ข ์ฐ์ถ๋ฌผ] | |
| - causes: 2~3๊ฐ. ๊ฐ ์์ธ์ name / pct(๊ธฐ์ฌ๋ %) / evidence(๊ตฌ์ฒด์ ๊ทผ๊ฑฐ) / citations(๋ฌธ์ ID ๋๋ incident ID) | |
| - pct ํฉ์ 100์ ๊ฐ๊น์์ผ ํจ, ๊ธฐ์ฌ๋ ๋์ ์์ธ๋ถํฐ ์ ๋ ฌ | |
| - ์ ๊ณต๋์ง ์์ ์ ๋ณด๋ ์ธ์ฉํ์ง ๋ง์ธ์""" | |
| def _execute_tier2_plan(plan_tier2: dict, trace_calls: list) -> str: | |
| """Planner๊ฐ ์ง์ ํ tool๋ค์ ์ง์ ํธ์ถํ๊ณ ์ปจํ ์คํธ๋ก ๋ณํ""" | |
| blocks = [] | |
| for query in plan_tier2.get("search_queries", []): | |
| result = dispatch_tool("search_knowledge", {"query": query}) | |
| trace_calls.append({"name": "search_knowledge", "args": {"query": query}}) | |
| blocks.append(f"[search_knowledge: {query!r}]\n{result}") | |
| for symptom in plan_tier2.get("incident_symptoms", []): | |
| result = dispatch_tool("lookup_incident_history", {"symptom": symptom}) | |
| trace_calls.append({"name": "lookup_incident_history", "args": {"symptom": symptom}}) | |
| blocks.append(f"[lookup_incident_history: {symptom!r}]\n{result}") | |
| for eq_id in plan_tier2.get("equipment_ids", []): | |
| result = 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{result}") | |
| return "\n\n".join(blocks) if blocks else "(planner๊ฐ ์ ๋ณด ์์ง ์ง์ ์์)" | |
| def _run_cause_conductor(alarm: dict, tier1: Tier1, plan: dict, trace: dict | None) -> Tier2: | |
| """Conductor ๋ชจ๋: plan๋๋ก tool ์คํ + 1ํ LLM synthesis""" | |
| tool_log: list[dict] = [] | |
| knowledge = _execute_tier2_plan(plan.get("tier2", {}), tool_log) | |
| sensors = ", ".join(f["name"] for f in tier1["features"]) | |
| user_prompt = f"""## ์ด์ ์๋ | |
| - ๊ณต์ : {alarm['title']} | |
| - lot: {alarm['lot_id']} | |
| - ์ด์ ํผ์ฒ: {alarm.get('feature')} {alarm.get('feature_arrow') or ''} | |
| ## Tier 1 ์ด์ ํ์ง ๊ฒฐ๊ณผ | |
| - ์ด์ ์ ์: {tier1['score']} | |
| - ๊ธฐ์ฌ ์ผ์(Top): {sensors} | |
| ## ์์ง๋ ์ปจํ ์คํธ (Planner ์ง์ ) | |
| {knowledge} | |
| ์ ์ปจํ ์คํธ๋ง ์ฌ์ฉํด ์์ธ์ ๋ถ์ํ์ธ์.""" | |
| 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": "tier2", "schema": TIER2_SCHEMA, "strict": True}, | |
| }, | |
| ) | |
| result = json.loads(resp.choices[0].message.content) | |
| if trace is not None: | |
| trace["tool_calls"] = tool_log | |
| trace["iterations"] = 0 | |
| trace["llm_calls"] = 1 | |
| trace["mode"] = "conductor" | |
| return result | |
| def run_cause( | |
| alarm: dict, | |
| tier1: Tier1, | |
| trace: dict | None = None, | |
| retry_hint: bool = False, | |
| plan: dict | None = None, | |
| ) -> Tier2: | |
| """์์ธ ๋ถ์ ์คํ | |
| plan ์ ๊ณต ์ conductor ๋ชจ๋ (๋จ์ผ LLM ํธ์ถ, tool ์ง์ ์คํ). | |
| plan=None์ด๋ฉด ๊ธฐ์กด autonomous tool-calling loop ๋ชจ๋. | |
| retry_hint=True ๋ฉด autonomous ๋ชจ๋์์ ๋ ์ ๊ทน์ ์ผ๋ก ๋๊ตฌ๋ฅผ ํธ์ถํ๋๋ก ์ ๋. | |
| """ | |
| if plan is not None: | |
| return _run_cause_conductor(alarm, tier1, plan, trace) | |
| user_prompt = _initial_user_prompt(alarm, tier1) | |
| if retry_hint: | |
| user_prompt += ( | |
| "\n\n[์ฌ์๋ ์ ํธ] ์ง์ ๋ถ์์์ ์ต์์ ์์ธ์ ๊ธฐ์ฌ๋๊ฐ ๋ฎ๊ฒ ์ฐ์ ๋์์ต๋๋ค. " | |
| "๋๊ตฌ๋ฅผ ๋ ์ ๊ทน์ ์ผ๋ก(๋ค์ํ ์ฟผ๋ฆฌยท์ฆ์ ํค์๋๋ก ์ฌ๋ฌ ๋ฒ) ํธ์ถํด ๋ ๊ฐํ ๊ทผ๊ฑฐ๋ฅผ ๋ชจ์ผ์ธ์." | |
| ) | |
| messages = [ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": user_prompt}, | |
| ] | |
| 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_CAUSE, | |
| 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}) | |
| # synthesis ํธ์ถ: tools ์์ด structured output ๊ฐ์ | |
| messages.append({ | |
| "role": "user", | |
| "content": "์์งํ ์ ๋ณด๋ฅผ ์ข ํฉํด ์ต์ข ์์ธ ๋ถ์์ JSON ์คํค๋ง์ ๋ง์ถฐ ์ถ๋ ฅํด ์ฃผ์ธ์.", | |
| }) | |
| final = client().chat.completions.create( | |
| model=SUBAGENT_MODEL, | |
| messages=messages, | |
| response_format={ | |
| "type": "json_schema", | |
| "json_schema": {"name": "tier2", "schema": TIER2_SCHEMA, "strict": True}, | |
| }, | |
| ) | |
| result = json.loads(final.choices[0].message.content) | |
| if trace is not None: | |
| trace["tool_calls"] = tool_call_log | |
| trace["iterations"] = iterations | |
| trace["llm_calls"] = iterations + 1 | |
| trace["mode"] = "autonomous" | |
| return result | |