# ============================================================ # 多智能体编排 Mesh(moduleId: core-agent-mesh, 可重生 ✅) # 演示与编排层:按需创建智能体、挂 Goal、按能力边界分发任务、 # 智能体消息互通、RUNNING 超期自动重建智能体、Goal 看门狗。 # 诚实边界:任务通过 handle_intent 走真实业务管线(分解/下达/排产), # P2 确认卡由执行智能体以 mesh: 身份自动批准并全程审计留痕。 # 落盘:/data/agent_mesh.json(可用 APS_MESH_PATH 覆盖) # ============================================================ from __future__ import annotations import asyncio import json import logging import os import tempfile import threading import time import uuid from datetime import datetime from typing import Any from server.timeutil import fmt_dt logger = logging.getLogger("aps.mesh") # ---------------- 角色模板(能力边界) ---------------- ROLE_TEMPLATES: dict[str, dict[str, Any]] = { "data-analyst": { "label": "数据解析智能体", "capabilities": ["folder.analyze", "data.analyze", "master.query", "plan.trace"], "desc": "解析工程目录/表格,校验排产齐备度", }, "mrp-planner": { "label": "MRP分解智能体", "capabilities": ["order.decompose", "mrp.release"], "desc": "按 BOM/供需净算产出采购/委外建议并下达", }, "scheduler": { "label": "柔性排产智能体", "capabilities": ["flex.schedule", "flex.capacity", "flex.reschedule"], "desc": "能力池组虚拟产线,闭环求解排产", }, "reporter": { "label": "报告智能体", "capabilities": ["report.generate"], "desc": "生成排产分析报告", }, } # 看门狗系统智能体(内置,不可删) WATCHDOG_AGENT_ID = "agent-watchdog" WATCHDOG_NAME = "Goal看门狗" # Goal 模板:APS 排产全流程(任务带能力意图与依赖) GOAL_TEMPLATES: dict[str, dict[str, Any]] = { "aps-full-flow": { "label": "APS 排产全流程", "tasks": [ {"key": "analyze", "title": "解析工程目录数据", "intent": "data.analyze", "params": {}, "dependsOn": []}, {"key": "decompose", "title": "执行MRP分解", "intent": "order.decompose", "params": {}, "dependsOn": ["analyze"]}, {"key": "release", "title": "下达采购/委外建议单", "intent": "mrp.release", "params": {}, "dependsOn": ["decompose"]}, {"key": "schedule", "title": "跑一版柔性排产", "intent": "flex.schedule", "params": {}, "dependsOn": ["release"]}, {"key": "report", "title": "生成排产分析报告", "intent": "report.generate", "params": {"reportType": "plan"}, "dependsOn": ["schedule"]}, ], }, } _DEFAULT_STALL_TIMEOUT_SEC = 45.0 _DEFAULT_MAX_RETRIES = 1 def _now() -> str: return fmt_dt(datetime.now()) def _uid(prefix: str) -> str: return f"{prefix}-{uuid.uuid4().hex[:8]}" def _mesh_path() -> str: override = os.environ.get("APS_MESH_PATH") if override: return override try: from server.aps_home import aps_home return str(aps_home() / "data" / "agent_mesh.json") except Exception: return os.path.join("server", "data", "agent_mesh.json") class MeshStore: """多智能体编排仓:线程安全 + 原子落盘。""" def __init__(self, path: str | None = None) -> None: self.path = path or _mesh_path() self._lock = threading.RLock() self.data: dict[str, Any] = self._load() self._ensure_watchdog() # ---------------- 持久化 ---------------- def _load(self) -> dict[str, Any]: try: with open(self.path, encoding="utf-8") as fh: data = json.load(fh) if isinstance(data, dict): data.setdefault("agents", []) data.setdefault("goals", []) data.setdefault("messages", []) data.setdefault("watchdog", {}) return data except (OSError, json.JSONDecodeError): pass return {"agents": [], "goals": [], "messages": [], "watchdog": {}} def save(self) -> None: with self._lock: os.makedirs(os.path.dirname(self.path) or ".", exist_ok=True) fd, tmp = tempfile.mkstemp(dir=os.path.dirname(self.path) or ".", suffix=".tmp") try: with os.fdopen(fd, "w", encoding="utf-8") as fh: json.dump(self.data, fh, ensure_ascii=False, indent=1) os.replace(tmp, self.path) except OSError: try: os.unlink(tmp) except OSError: pass def _ensure_watchdog(self) -> None: with self._lock: if not any(a.get("id") == WATCHDOG_AGENT_ID for a in self.data["agents"]): self.data["agents"].insert(0, { "id": WATCHDOG_AGENT_ID, "name": WATCHDOG_NAME, "role": "watchdog", "label": "Goal看门狗(系统)", "capabilities": ["mesh.watchdog"], "status": "IDLE", "system": True, "createdBy": "SYSTEM", "createdAt": _now(), "heartbeatAt": _now(), }) wd = self.data["watchdog"] wd.setdefault("enabled", True) wd.setdefault("stallTimeoutSec", _DEFAULT_STALL_TIMEOUT_SEC) wd.setdefault("maxRetries", _DEFAULT_MAX_RETRIES) wd.setdefault("lastTickAt", None) wd.setdefault("alerts", []) # ---------------- 智能体 ---------------- def create_agent(self, name: str, role: str, capabilities: list[str] | None = None, *, created_by: str = "USER", parent_id: str | None = None) -> dict[str, Any]: tpl = ROLE_TEMPLATES.get(role, {}) caps = capabilities if capabilities is not None else list(tpl.get("capabilities") or []) agent = { "id": _uid("agent"), "name": name.strip() or tpl.get("label") or role, "role": role, "label": tpl.get("label") or role, "capabilities": caps, "status": "IDLE", "system": False, "createdBy": created_by, "parentId": parent_id, "createdAt": _now(), "heartbeatAt": _now(), "currentTaskId": None, } with self._lock: self.data["agents"].append(agent) self.save() return agent def _capable_idle(self, intent: str) -> dict[str, Any] | None: for agent in self.data["agents"]: if agent.get("system"): continue if agent.get("status") == "IDLE" and intent in (agent.get("capabilities") or []): return agent return None def _spawn_for_intent(self, intent: str, *, created_by: str, goal_id: str) -> dict[str, Any] | None: """没有能执行该意图的空闲智能体时,按角色模板自动新建(允许新建智能体)。""" for role, tpl in ROLE_TEMPLATES.items(): if intent in (tpl.get("capabilities") or []): seq = sum(1 for a in self.data["agents"] if a.get("role") == role) + 1 agent = self.create_agent( f"{tpl['label']}-{seq}", role, created_by=created_by, parent_id=None, ) self.post_message( from_agent_id=WATCHDOG_AGENT_ID, to_agent_id=None, goal_id=goal_id, kind="INFO", text=f"没有空闲的「{tpl['label']}」,已自动新建 {agent['name']} 接管 {intent}。", ) return agent return None # ---------------- Goal / 任务 ---------------- def create_goal(self, title: str, *, template: str | None = "aps-full-flow", tasks: list[dict[str, Any]] | None = None, created_by: str = "USER", world_key: str | None = None, session_id: str | None = None) -> dict[str, Any]: tpl = GOAL_TEMPLATES.get(template or "", {}) raw_tasks = tasks if tasks is not None else tpl.get("tasks") or [] goal_id = _uid("goal") task_rows = [] for spec in raw_tasks: task_rows.append({ "id": _uid("task"), "goalId": goal_id, "key": spec.get("key") or spec.get("intent"), "title": spec.get("title") or spec.get("intent"), "intent": spec["intent"], "params": dict(spec.get("params") or {}), "dependsOn": list(spec.get("dependsOn") or []), "status": "PENDING", "assigneeAgentId": None, "assigneeName": None, "attempts": 0, "result": None, "error": None, "startedAt": None, "finishedAt": None, }) goal = { "id": goal_id, "title": title.strip() or tpl.get("label") or "未命名 Goal", "template": template, "status": "ACTIVE", "createdBy": created_by, "worldKey": world_key, "sessionId": session_id, "createdAt": _now(), "tasks": task_rows, "watchdog": {"stallTimeoutSec": float(self.data["watchdog"].get("stallTimeoutSec") or _DEFAULT_STALL_TIMEOUT_SEC), "maxRetries": int(self.data["watchdog"].get("maxRetries") or _DEFAULT_MAX_RETRIES)}, } with self._lock: self.data["goals"].append(goal) self.save() self.post_message(from_agent_id=WATCHDOG_AGENT_ID, to_agent_id=None, goal_id=goal_id, kind="INFO", text=f"Goal「{goal['title']}」已挂载,{len(task_rows)} 个任务进入看门狗监控。") return goal def get_goal(self, goal_id: str) -> dict[str, Any] | None: return next((g for g in self.data["goals"] if g.get("id") == goal_id), None) # ---------------- 消息总线 ---------------- def post_message(self, *, from_agent_id: str, to_agent_id: str | None, goal_id: str | None, kind: str, text: str) -> dict[str, Any]: msg = { "id": _uid("msg"), "fromAgentId": from_agent_id, "fromName": self._agent_name(from_agent_id), "toAgentId": to_agent_id, "toName": self._agent_name(to_agent_id) if to_agent_id else "全体", "goalId": goal_id, "kind": kind, "text": text, "createdAt": _now(), } with self._lock: self.data["messages"].append(msg) self.data["messages"] = self.data["messages"][-300:] self.save() return msg def _agent_name(self, agent_id: str | None) -> str: if not agent_id: return "?" agent = next((a for a in self.data["agents"] if a.get("id") == agent_id), None) return (agent or {}).get("name") or agent_id # ---------------- 看门狗 ---------------- def watchdog_tick(self) -> dict[str, Any]: """扫描 RUNNING 超期任务:标记 STALLED → 自动重试/失败 → Goal 升级。幂等可频繁调用。""" wd = self.data["watchdog"] wd["lastTickAt"] = _now() if not wd.get("enabled", True): self.save() return {"enabled": False, "stalled": 0} stalled = 0 now = time.time() for goal in self.data["goals"]: if goal.get("status") not in ("ACTIVE", "ATTENTION"): continue gwd = goal.get("watchdog") or {} timeout = float(gwd.get("stallTimeoutSec") or _DEFAULT_STALL_TIMEOUT_SEC) max_retries = int(gwd.get("maxRetries") or _DEFAULT_MAX_RETRIES) for task in goal.get("tasks") or []: if task.get("status") != "RUNNING" or not task.get("startedAt"): continue try: started = datetime.strptime(task["startedAt"], "%Y-%m-%d %H:%M:%S").timestamp() except (ValueError, TypeError): continue if now - started <= timeout: continue stalled += 1 attempts = int(task.get("attempts") or 0) assignee = task.get("assigneeName") or "未分配" if attempts <= max_retries: task["status"] = "PENDING" task["startedAt"] = None task["assigneeAgentId"] = None task["assigneeName"] = None self.post_message( from_agent_id=WATCHDOG_AGENT_ID, to_agent_id=None, goal_id=goal["id"], kind="ALERT", text=f"任务「{task['title']}」在 {assignee} 上超过 {timeout:.0f}s 未完成,已收回并重新排队(第 {attempts} 次重试)。") else: task["status"] = "FAILED" task["error"] = f"看门狗:超过 {timeout:.0f}s 未完成且重试耗尽" task["finishedAt"] = _now() goal["status"] = "ATTENTION" self.post_message( from_agent_id=WATCHDOG_AGENT_ID, to_agent_id=None, goal_id=goal["id"], kind="ALERT", text=f"任务「{task['title']}」重试 {max_retries} 次仍超时,标记 FAILED,Goal「{goal['title']}」升级为 ATTENTION。") if stalled: wd.setdefault("alerts", []).append({"at": _now(), "stalled": stalled}) wd["alerts"] = wd["alerts"][-50:] self.save() return {"enabled": True, "stalled": stalled, "lastTickAt": wd["lastTickAt"]} def configure_watchdog(self, *, stall_timeout_sec: float | None = None, max_retries: int | None = None, enabled: bool | None = None) -> dict[str, Any]: wd = self.data["watchdog"] if stall_timeout_sec is not None: wd["stallTimeoutSec"] = max(5.0, float(stall_timeout_sec)) if max_retries is not None: wd["maxRetries"] = max(0, int(max_retries)) if enabled is not None: wd["enabled"] = bool(enabled) self.save() return wd # ---------------- 快照 ---------------- def reset(self) -> dict[str, Any]: """恢复起始状态:清空 Goal/消息/告警,只保留系统看门狗智能体。""" with self._lock: self.data["agents"] = [a for a in self.data["agents"] if a.get("system")] self.data["goals"] = [] self.data["messages"] = [] self.data["watchdog"]["alerts"] = [] self.save() return self.snapshot() def snapshot(self) -> dict[str, Any]: with self._lock: return json.loads(json.dumps({ "agents": self.data["agents"], "goals": self.data["goals"], "messages": self.data["messages"][-100:], "watchdog": self.data["watchdog"], "roleTemplates": [ {"role": role, **{k: v for k, v in tpl.items()}} for role, tpl in ROLE_TEMPLATES.items() ], "goalTemplates": [ {"key": key, "label": tpl["label"], "taskCount": len(tpl["tasks"])} for key, tpl in GOAL_TEMPLATES.items() ], })) _MESH: MeshStore | None = None _MESH_LOCK = threading.Lock() def get_mesh() -> MeshStore: global _MESH with _MESH_LOCK: if _MESH is None: _MESH = MeshStore() return _MESH # ---------------- 分发执行(真实业务管线) ---------------- _DISPATCH_THREADS: dict[str, threading.Thread] = {} def dispatch_goal(goal_id: str, *, session_id: str | None = None) -> dict[str, Any]: """异步分发 Goal:按依赖序把任务分给有能力的空闲智能体,必要时自动新建。""" mesh = get_mesh() goal = mesh.get_goal(goal_id) if goal is None: raise ValueError(f"Goal 不存在:{goal_id}") # Goal 绑定的项目世界与当前活动世界不一致时先切换,保证任务打在正确的数据边界上 goal_world = (goal.get("worldKey") or "").strip() if goal_world: try: from server.state.store import get_store, switch_store if getattr(get_store(), "world_key", "default") != goal_world: switch_store(goal_world) except Exception: # noqa: BLE001 - 切换失败尽力而为,仍在当前世界执行 logger.exception("mesh: switch store to %s failed", goal_world) eff_session = session_id or (goal.get("sessionId") or "") or f"mesh-{goal_id}" try: from server.auth.context import get_identity identity = get_identity() except Exception: # noqa: BLE001 - 无请求身份时用系统兜底 identity = None running = DispatchThread(goal_id=goal_id, session_id=eff_session, world_key=goal_world or None, identity=identity) if goal_id in _DISPATCH_THREADS and _DISPATCH_THREADS[goal_id].is_alive(): return {"message": "该 Goal 已在分发执行中", "goalId": goal_id, "alreadyRunning": True} _DISPATCH_THREADS[goal_id] = running running.start() return {"message": f"Goal「{goal['title']}」已开始分发执行", "goalId": goal_id, "alreadyRunning": False} class DispatchThread(threading.Thread): def __init__(self, *, goal_id: str, session_id: str, world_key: str | None = None, identity: Any = None) -> None: super().__init__(daemon=True, name=f"mesh-dispatch-{goal_id}") self.goal_id = goal_id self.session_id = session_id self.world_key = world_key self.identity = identity def run(self) -> None: # noqa: D102 mesh = get_mesh() goal = mesh.get_goal(self.goal_id) if goal is None: return token = None if self.identity is not None: try: from server.auth.context import bind_identity token = bind_identity(self.identity) # 项目仓按用户隔离:绑回创建时身份 except Exception: # noqa: BLE001 token = None try: while True: task = self._next_ready(goal) if task is None: break self._run_task(mesh, goal, task) self._finish_goal(mesh, goal) except Exception as exc: # noqa: BLE001 - 编排兜底:任何异常落 Goal,不炸线程 logger.exception("mesh dispatch failed") goal["status"] = "ATTENTION" mesh.post_message(from_agent_id=WATCHDOG_AGENT_ID, to_agent_id=None, goal_id=self.goal_id, kind="ALERT", text=f"分发执行异常:{exc}") mesh.save() finally: if token is not None: try: from server.auth.context import reset_identity reset_identity(token) except Exception: # noqa: BLE001 pass def _next_ready(self, goal: dict[str, Any]) -> dict[str, Any] | None: done_keys = {t["key"] for t in goal["tasks"] if t.get("status") == "DONE"} for task in goal["tasks"]: if task.get("status") == "PENDING" and all(dep in done_keys for dep in task.get("dependsOn") or []): return task # 还有 RUNNING(看门狗收回中会重新 PENDING)则等待 if any(t.get("status") == "RUNNING" for t in goal["tasks"]): time.sleep(1.0) return self._next_ready(goal) return None def _run_task(self, mesh: MeshStore, goal: dict[str, Any], task: dict[str, Any]) -> None: intent = task["intent"] agent = mesh._capable_idle(intent) or mesh._spawn_for_intent( intent, created_by=f"GOAL:{goal['id']}", goal_id=goal["id"]) if agent is None: task["status"] = "FAILED" task["error"] = f"没有任何角色模板能执行 {intent}" task["finishedAt"] = _now() goal["status"] = "ATTENTION" mesh.save() return task["status"] = "RUNNING" task["assigneeAgentId"] = agent["id"] task["assigneeName"] = agent["name"] task["attempts"] = int(task.get("attempts") or 0) + 1 task["startedAt"] = _now() agent["status"] = "RUNNING" agent["currentTaskId"] = task["id"] agent["heartbeatAt"] = _now() mesh.save() mesh.post_message(from_agent_id=WATCHDOG_AGENT_ID, to_agent_id=agent["id"], goal_id=goal["id"], kind="REQUEST", text=f"分发任务「{task['title']}」({intent}),第 {task['attempts']} 次尝试。") try: text, blocks = _execute_intent(self.session_id, intent, task.get("params") or {}, agent, world_key=self.world_key) # P2 确认卡:执行智能体以自己身份批准(审计 actor=mesh:) for block in blocks or []: confirm_id = ((block.get("props") or {}).get("confirmId")) if not confirm_id: continue text += "\n" + _auto_confirm(confirm_id, agent) # 排产/分解被阻断(fail-closed 文案)不算任务成功:落 FAILED 交看门狗升级 if "没有可下达的草稿建议单" in text and _released_supply_exists(self.world_key): text = "供应建议单此前已下达(RELEASED 在账),本次幂等跳过。" elif "被阻断" in text or "无法生成" in text or "没有可下达" in text: raise RuntimeError(text.strip().splitlines()[0][:200]) task["status"] = "DONE" task["result"] = (text or "")[:600] task["finishedAt"] = _now() agent["status"] = "IDLE" agent["currentTaskId"] = None agent["heartbeatAt"] = _now() mesh.save() nxt = self._peek_next(goal, task) summary_line = next( (ln for ln in reversed((text or "").splitlines()) if ln.strip()), "") mesh.post_message(from_agent_id=agent["id"], to_agent_id=None, goal_id=goal["id"], kind="RESPONSE", text=f"「{task['title']}」完成 ✅ {summary_line[:80]}" + (f",@{nxt} 请接棒。" if nxt else ",Goal 任务全部就绪。")) except Exception as exc: # noqa: BLE001 - 任务失败落状态,交给看门狗/重试 logger.exception("mesh task failed: %s", intent) task["status"] = "FAILED" task["error"] = str(exc)[:300] task["finishedAt"] = _now() agent["status"] = "IDLE" agent["currentTaskId"] = None goal["status"] = "ATTENTION" mesh.save() mesh.post_message(from_agent_id=agent["id"], to_agent_id=WATCHDOG_AGENT_ID, goal_id=goal["id"], kind="ALERT", text=f"「{task['title']}」执行失败:{exc}") @staticmethod def _peek_next(goal: dict[str, Any], just_done: dict[str, Any]) -> str | None: done_keys = {t["key"] for t in goal["tasks"] if t.get("status") == "DONE"} for task in goal["tasks"]: if task.get("status") == "PENDING" and all(dep in done_keys for dep in task.get("dependsOn") or []): return task.get("title") return None def _finish_goal(self, mesh: MeshStore, goal: dict[str, Any]) -> None: statuses = {t.get("status") for t in goal["tasks"]} if statuses and statuses <= {"DONE"}: goal["status"] = "ACHIEVED" mesh.post_message(from_agent_id=WATCHDOG_AGENT_ID, to_agent_id=None, goal_id=goal["id"], kind="INFO", text=f"Goal「{goal['title']}」全部任务完成 ✅ 看门狗结案。") elif goal.get("status") != "ATTENTION": goal["status"] = "ATTENTION" mesh.save() def _execute_intent(session_id: str, intent: str, params: dict[str, Any], agent: dict[str, Any], *, world_key: str | None = None) -> tuple[str, list[dict[str, Any]]]: """通过真实业务管线执行意图(与对话同一入口 handle_intent)。 world_key 指定时先切到 Goal 绑定的项目世界:全局活动世界可能被 前端工作区同步抢占,必须在每个任务执行当下重新钉住数据边界。 """ from server.agent_core.intent import IntentResult from server.aps_domain.workflow import handle_intent from server.state.store import get_store, switch_store if world_key and getattr(get_store(), "world_key", "default") != world_key: switch_store(world_key) store = get_store() reply = asyncio.run(handle_intent( store, session_id, IntentResult(intent=intent, params=params, confidence=1.0), actor=f"mesh:{agent['name']}", )) blocks = [b.model_dump() for b in (reply.blocks or [])] return reply.text or "", blocks def _auto_confirm(confirm_id: str, agent: dict[str, Any]) -> str: from server.aps_domain.workflow import execute_confirmed from server.state.store import get_store return execute_confirmed(get_store(), confirm_id, True, actor=f"mesh:{agent['name']}", note="多智能体编排自动批准(审计留痕)") # ---------------- 看门狗后台循环 ---------------- _WATCHDOG_STARTED = False def start_watchdog_loop(interval_sec: float = 3.0) -> None: """启动看门狗后台节拍(幂等)。桌面/服务启动时调用一次。""" global _WATCHDOG_STARTED if _WATCHDOG_STARTED: return _WATCHDOG_STARTED = True def _loop() -> None: while True: try: get_mesh().watchdog_tick() except Exception: # noqa: BLE001 - 看门狗自身绝不能崩 logger.exception("mesh watchdog tick failed") time.sleep(interval_sec) threading.Thread(target=_loop, daemon=True, name="mesh-watchdog").start() def _released_supply_exists(world_key: str | None) -> bool: """目标世界是否已有非草稿(已下达)采购/委外单:幂等判断依据。""" try: from server.state.store import get_store, switch_store if world_key and getattr(get_store(), "world_key", "default") != world_key: switch_store(world_key) data = get_store().data for row in (data.get("purchaseOrders") or []) + (data.get("outsourceOrders") or []): if isinstance(row, dict) and row.get("status") and row.get("status") != "DRAFT": return True except Exception: # noqa: BLE001 - 判断失败保守返回 False(维持失败语义) pass return False