hee_!J
feat(conductor): Plan-and-Execute ํจํด - Central Planner + Tier executor (env AGENT_MODE)
3fb690f | """Tier 4 ๋์ ๊ถ๊ณ ์์ด์ ํธ | |
| ๋ ๋ชจ๋ ์ง์: | |
| 1. Autonomous (plan=None): LLM์ด tool์ ์์จ ํธ์ถํ๋ agent loop | |
| 2. Conductor (plan ์ ๊ณต): Planner๊ฐ ์ง์ ํ tool ํธ์ถ + ๋จ์ผ LLM synthesis | |
| ๊ฐ์ฉ ๋๊ตฌ: search_knowledge, lookup_incident_history, get_pm_history, check_pm_schedule | |
| """ | |
| import json | |
| from langsmith import traceable | |
| from agents.cause import _assistant_msg_dict | |
| from agents.llm import SUBAGENT_MODEL, client | |
| from agents.rag.store import load_document | |
| from agents.tools import TOOLS_RESPONSE, dispatch_tool | |
| from agents.tools.equipment import ALARM_EQUIPMENT | |
| from core.schema import Tier1, Tier2, Tier3, Tier4 | |
| MAX_TOOL_ITERATIONS = 4 | |
| LLM_PART_SCHEMA = { | |
| "type": "object", | |
| "properties": { | |
| "immediate": { | |
| "type": "array", | |
| "items": { | |
| "type": "object", | |
| "properties": { | |
| "text": {"type": "string"}, | |
| "meta": {"type": ["string", "null"]}, | |
| }, | |
| "required": ["text", "meta"], | |
| "additionalProperties": False, | |
| }, | |
| }, | |
| "longterm": { | |
| "type": "array", | |
| "items": { | |
| "type": "object", | |
| "properties": { | |
| "text": {"type": "string"}, | |
| "meta": {"type": ["string", "null"]}, | |
| }, | |
| "required": ["text", "meta"], | |
| "additionalProperties": False, | |
| }, | |
| }, | |
| "ref_doc_ids": { | |
| "type": "array", | |
| "items": {"type": "string"}, | |
| "description": "์ต์ข ๊ถ๊ณ ์ ๊ทผ๊ฑฐ๋ก ์ธ์ฉ๋ ๋ฌธ์ ID (SOP/INC/FMEA/FLOW)", | |
| }, | |
| }, | |
| "required": ["immediate", "longterm", "ref_doc_ids"], | |
| "additionalProperties": False, | |
| } | |
| SYSTEM_PROMPT = """๋น์ ์ ๋ฐ๋์ฒด ๊ณต์ ๋์ ๊ถ๊ณ ์ ๋ฌธ๊ฐ์ ๋๋ค. | |
| ์ด์ ๋จ๊ณ(ํ์งยท์์ธยท์ํฅ)์ ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ ์ค์ fab์์ ์คํ ๊ฐ๋ฅํ ์กฐ์น๋ฅผ ๊ถ๊ณ ํฉ๋๋ค. | |
| [๊ฐ์ฉ ๋๊ตฌ] | |
| - search_knowledge(query): SOP/INC/FMEA ๋ฌธ์ ๊ฒ์ - ํ์ค ์ ์ฐจ์ ๊ณผ๊ฑฐ ํด๊ฒฐ์ฑ ํ๋ณด | |
| - lookup_incident_history(symptom): ๊ณผ๊ฑฐ incident์ ์ค์ resolution (์์์๊ฐยทyield ํ๋ณต๋ฅ ํฌํจ) | |
| - get_pm_history(equipment_id): ์ฅ๋น PM overdue ์ฌ๋ถ - ์ฆ์ PM ํ์์ฑ ํ๋จ | |
| - check_pm_schedule(equipment_id): ๊ฐ์ฉ PM ์๋์ฐ - immediate ์กฐ์น์ ์คํ ๊ฐ๋ฅ ์์ ํ๋ณด | |
| [์ ๋ต] | |
| - ๋๊ตฌ๋ฅผ ์์จ ํธ์ถํด ๊ทผ๊ฑฐ SOPยท๊ณผ๊ฑฐ ํด๊ฒฐ์ฑ ยทPM ๊ฐ์ฉ์ฑ์ ํ์ธํ์ธ์ | |
| - ๊ถ๊ณ ๋ fab ํ์ฅ์์ ์ฆ์ ์คํ ๊ฐ๋ฅํ ํํ๋ก ์์ฑ (๋ชจํธํ 'OO ๊ฒํ '๋ณด๋ค 'OO ์ฆ์ ํฌ์ , ์์ N์๊ฐ') | |
| - meta ํ๋์๋ ์๊ฐยทhold ๋์ยทํ์กฐ ๋ถ์ ๋ฑ ์ด์ ์ ํธ๋ฅผ ๋ฃ์ผ์ธ์ | |
| [์ต์ข ์ฐ์ถ๋ฌผ] | |
| - immediate: ์ฆ์ ์กฐ์น (์ ์๊ฐ ๋ด ์ํ) 2~3๊ฑด | |
| - longterm: ์ค์ฅ๊ธฐ ์กฐ์น (์ฌ๋ฐ ๋ฐฉ์ง, ์ ์ฐจ ๊ฐ์ ) 1~2๊ฑด | |
| - ref_doc_ids: ๊ถ๊ณ ๊ทผ๊ฑฐ๋ก ์ค์ ์ฌ์ฉํ ๋ฌธ์ ID ๋ชฉ๋ก (๋๊ตฌ๊ฐ ๋ฐํํ doc_id ๊ทธ๋๋ก)""" | |
| def _doc_description(doc_id: str) -> str: | |
| text = load_document(doc_id) | |
| if not text: | |
| return doc_id | |
| first_line = text.split("\n", 1)[0].lstrip("# ").strip() | |
| for sep in (" โ ", " - "): | |
| if sep in first_line: | |
| return first_line.split(sep, 1)[1].strip() | |
| return first_line | |
| def _initial_user_prompt(alarm: dict, tier1: Tier1, tier2: Tier2, tier3: Tier3) -> str: | |
| cause_lines = "\n".join(f"- {c['name']} ({c['pct']}%)" for c in tier2["causes"]) | |
| impact_lots_text = ", ".join( | |
| f"{l['label']} {l['lots']}lot/{l['wafers']}์ฅ" for l in tier3["impact_lots"] | |
| ) | |
| downstream_text = ", ".join( | |
| f"{d['stage']}({d.get('delta', '')})" for d in tier3["dependencies"] | |
| ) | |
| equipment_id = ALARM_EQUIPMENT.get(alarm["id"], "(๋ฏธ๋งคํ)") | |
| return f"""## ์ด์ ์๋ | |
| - ๊ณต์ : {alarm['title']} | |
| - lot: {alarm['lot_id']} | |
| - ์๋ ID: {alarm['id']} | |
| - ์ถ์ ์ฅ๋น ID: {equipment_id} | |
| ## Tier 1 ์ด์ ํ์ง | |
| - ์ด์ ์ ์: {tier1['score']} | |
| ## Tier 2 ์์ธ (๊ธฐ์ฌ๋ ์) | |
| {cause_lines} | |
| ## Tier 3 ์ํฅ | |
| - ์์ ์์จ ์์ค: {tier3['yield_loss']} %p | |
| - downstream: {downstream_text} | |
| - ์ํฅ WIP: {impact_lots_text} | |
| ์ ๋ถ์์ ์ข ํฉํด immediate์ longterm ์กฐ์น๋ฅผ ๊ถ๊ณ ํด ์ฃผ์ธ์. | |
| ํ์ํ SOPยท๊ณผ๊ฑฐ ํด๊ฒฐ์ฑ ยทPM ์๋์ฐ๋ ๋๊ตฌ๋ฅผ ํธ์ถํด ์์จ์ ์ผ๋ก ์์งํ์ธ์.""" | |
| CONDUCTOR_SYSTEM_PROMPT = """๋น์ ์ ๋ฐ๋์ฒด ๊ณต์ ๋์ ๊ถ๊ณ ์ ๋ฌธ๊ฐ์ ๋๋ค. | |
| Central Planner๊ฐ ์ด๋ฏธ ํ์ํ ์ ๋ณด(SOPยท๊ณผ๊ฑฐ ํด๊ฒฐ์ฑ ยทPM ์๋์ฐ)๋ฅผ ๋ชจ๋ ์์งํด [์์ง๋ ์ปจํ ์คํธ]์ ์ ๋ฆฌํด ๋์์ต๋๋ค. | |
| ๋น์ ์ ์ถ๊ฐ ๋๊ตฌ ํธ์ถ ์์ด ๊ทธ ์ปจํ ์คํธ๋ง ์ฌ์ฉํด ์ฐ์ถํ์ธ์. | |
| [์ต์ข ์ฐ์ถ๋ฌผ] | |
| - immediate: ์ฆ์ ์กฐ์น 2~3๊ฑด (์ ์๊ฐ ๋ด ์คํ ๊ฐ๋ฅ, meta์ ์๊ฐยทhold ๋์ยท๋ถ์ ๋ช ์) | |
| - longterm: ์ค์ฅ๊ธฐ ์กฐ์น 1~2๊ฑด | |
| - ref_doc_ids: ๊ถ๊ณ ๊ทผ๊ฑฐ๋ก ์ค์ ์ฌ์ฉํ ๋ฌธ์ ID (search_knowledge๊ฐ ๋ฐํํ doc_id๋ง)""" | |
| def _execute_tier4_plan(plan_tier4: dict, trace_calls: list) -> tuple[str, list[str]]: | |
| """Planner๊ฐ ์ง์ ํ tool๋ค์ ์ง์ ํธ์ถ, ์ปจํ ์คํธ + ๊ฒ์ doc_id ๋ฆฌ์คํธ ๋ฐํ""" | |
| blocks = [] | |
| found_docs: list[str] = [] | |
| for query in plan_tier4.get("search_queries", []): | |
| r = dispatch_tool("search_knowledge", {"query": query}) | |
| trace_calls.append({"name": "search_knowledge", "args": {"query": query}}) | |
| blocks.append(f"[search_knowledge: {query!r}]\n{r}") | |
| try: | |
| parsed = json.loads(r) | |
| found_docs.extend(h["doc_id"] for h in parsed.get("hits", [])) | |
| except (json.JSONDecodeError, KeyError): | |
| pass | |
| for symptom in plan_tier4.get("incident_symptoms", []): | |
| r = 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{r}") | |
| for eq_id in plan_tier4.get("equipment_ids", []): | |
| r1 = 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{r1}") | |
| r2 = dispatch_tool("check_pm_schedule", {"equipment_id": eq_id}) | |
| trace_calls.append({"name": "check_pm_schedule", "args": {"equipment_id": eq_id}}) | |
| blocks.append(f"[check_pm_schedule: {eq_id!r}]\n{r2}") | |
| return "\n\n".join(blocks) if blocks else "(planner๊ฐ ์ ๋ณด ์์ง ์ง์ ์์)", found_docs | |
| def _run_response_conductor( | |
| alarm: dict, tier1: Tier1, tier2: Tier2, tier3: Tier3, | |
| plan: dict, trace: dict | None, | |
| ) -> Tier4: | |
| tool_log: list[dict] = [] | |
| knowledge, found_docs = _execute_tier4_plan(plan.get("tier4", {}), tool_log) | |
| cause_lines = "\n".join(f"- {c['name']} ({c['pct']}%)" for c in tier2["causes"]) | |
| impact_lots_text = ", ".join( | |
| f"{l['label']} {l['lots']}lot/{l['wafers']}์ฅ" for l in tier3["impact_lots"] | |
| ) | |
| user_prompt = f"""## ์ด์ ์๋ | |
| - ๊ณต์ : {alarm['title']}, lot: {alarm['lot_id']} | |
| ## Tier 1: ์ด์ ์ ์ {tier1['score']} | |
| ## Tier 2 ์์ธ (๊ธฐ์ฌ๋ ์) | |
| {cause_lines} | |
| ## Tier 3 ์ํฅ | |
| - ์์ ์์จ ์์ค: {tier3['yield_loss']} %p | |
| - ์ํฅ WIP: {impact_lots_text} | |
| ## ์์ง๋ ์ปจํ ์คํธ (Planner ์ง์ ) | |
| {knowledge} | |
| ์ ์ปจํ ์คํธ๋ง ์ฌ์ฉํด immediate, longterm, ref_doc_ids๋ฅผ ์ฐ์ถํ์ธ์.""" | |
| 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": "tier4_part", "schema": LLM_PART_SCHEMA, "strict": True}, | |
| }, | |
| ) | |
| llm_out = json.loads(resp.choices[0].message.content) | |
| ref_ids = llm_out.get("ref_doc_ids") or found_docs[:4] | |
| refs = [{"id": d, "desc": _doc_description(d)} for d in ref_ids if d] | |
| if trace is not None: | |
| trace["tool_calls"] = tool_log | |
| trace["iterations"] = 0 | |
| trace["llm_calls"] = 1 | |
| trace["mode"] = "conductor" | |
| return { | |
| "immediate": llm_out["immediate"], | |
| "longterm": llm_out["longterm"], | |
| "refs": refs, | |
| } | |
| def run_response( | |
| alarm: dict, tier1: Tier1, tier2: Tier2, tier3: Tier3, | |
| trace: dict | None = None, | |
| plan: dict | None = None, | |
| ) -> Tier4: | |
| """Tier 4 ๋์ ๊ถ๊ณ . plan ์ ๊ณต ์ conductor ๋ชจ๋(LLM 1ํ), ์๋๋ฉด autonomous loop.""" | |
| if plan is not None: | |
| return _run_response_conductor(alarm, tier1, tier2, tier3, plan, trace) | |
| # autonomous ๋ชจ๋ (LLM์ด tool์ ์์จ ํธ์ถํ๋ ๊ธฐ์กด loop) | |
| messages = [ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": _initial_user_prompt(alarm, tier1, tier2, tier3)}, | |
| ] | |
| tool_call_log: list[dict] = [] | |
| iterations = 0 | |
| cited_doc_ids: list[str] = [] | |
| for iterations in range(1, MAX_TOOL_ITERATIONS + 1): | |
| resp = client().chat.completions.create( | |
| model=SUBAGENT_MODEL, | |
| messages=messages, | |
| tools=TOOLS_RESPONSE, | |
| 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}) | |
| # search_knowledge๋ก ๋ฐ์ doc_id ํธ๋ํน (ref ํ๋ณด) | |
| if tc.function.name == "search_knowledge": | |
| try: | |
| parsed = json.loads(result) | |
| cited_doc_ids.extend(h["doc_id"] for h in parsed.get("hits", [])) | |
| except (json.JSONDecodeError, KeyError): | |
| pass | |
| messages.append({"role": "tool", "tool_call_id": tc.id, "content": result}) | |
| messages.append({ | |
| "role": "user", | |
| "content": "์์งํ ์ ๋ณด๋ฅผ ์ข ํฉํด immediate, longterm, ref_doc_ids๋ฅผ JSON ์คํค๋ง์ ๋ง์ถฐ ์ถ๋ ฅํด ์ฃผ์ธ์.", | |
| }) | |
| final = client().chat.completions.create( | |
| model=SUBAGENT_MODEL, | |
| messages=messages, | |
| response_format={ | |
| "type": "json_schema", | |
| "json_schema": {"name": "tier4_part", "schema": LLM_PART_SCHEMA, "strict": True}, | |
| }, | |
| ) | |
| llm_out = json.loads(final.choices[0].message.content) | |
| # LLM์ด ๋ช ์ํ ref_doc_ids๋ง refs๋ก ๊ตฌ์ฑ (search๋ก ๋ณธ ๋ชจ๋ hits๊ฐ ์๋๋ผ ์ค์ ์ฌ์ฉํ ๊ฒ) | |
| ref_ids = llm_out.get("ref_doc_ids") or cited_doc_ids[:4] | |
| refs = [{"id": d, "desc": _doc_description(d)} for d in ref_ids if d] | |
| if trace is not None: | |
| trace["tool_calls"] = tool_call_log | |
| trace["iterations"] = iterations | |
| trace["llm_calls"] = iterations + 1 | |
| trace["mode"] = "autonomous" | |
| return { | |
| "immediate": llm_out["immediate"], | |
| "longterm": llm_out["longterm"], | |
| "refs": refs, | |
| } | |