# ============================================================ # 外部算法引擎适配器(moduleId: engines-external, 可重生 ✅) # ISchedulingEngine:组中性 DTO → HTTP/local skill → 写回 world # ============================================================ from __future__ import annotations from typing import Any import httpx from server.aps_domain.scheduling_dto import ( SchedulingSolution, apply_flex_solution, world_to_flex_problem, ) from server.contracts import ScheduleResult from server.engines.base import EngineParams, ISchedulingEngine class ExternalEngine(ISchedulingEngine): """外部算法排产:通过 SkillRegistry 选定 skill,HTTP 或 local://stub。""" name = "EXTERNAL" supports_anytime = False def __init__(self, skill_id: str | None = None) -> None: self.skill_id = skill_id def solve(self, world: dict[str, Any], params: EngineParams, next_id) -> ScheduleResult: from server.agent_core.skills import get_skills skill_id = self.skill_id or (params.meta_skill_id if hasattr(params, "meta_skill_id") else None) # EngineParams 无自定义字段时从 constraints 或 name 透传;workflow 会设 world 临时键 skill_id = skill_id or world.pop("_externalSkillId", None) track = "flex" # 本切片外部 skill 主路径走柔性轨 reg = get_skills() skill = reg.get_enabled(skill_id, track=track) or reg.get_enabled(skill_id) or reg.get_enabled() if not skill: raise ValueError("没有可用的外部算法 skill(请先登记并启用)") order_ids = list(params.orderIds or []) or None problem = world_to_flex_problem( world, order_ids=order_ids, start_time=params.startDate + " 08:00" if params.startDate and " " not in params.startDate else params.startDate, ) solution = self._call_skill(skill, problem.model_dump()) summary = apply_flex_solution(world, solution, next_id, skill_id=skill["skill_id"]) return ScheduleResult( versionId=summary["versionId"], versionNo=summary["versionNo"], engineType="EXTERNAL", strategy=f"EXTERNAL:{skill['skill_id']}", status="DRAFT", orderCount=len(problem.orders), poCount=summary["vlCount"], woCount=summary["woCount"], conflictCount=summary["conflictCount"], totalTardiness=float((solution.kpi or {}).get("totalTardiness") or 0), avgUtilization=float(summary.get("avgUtilization") or 0), evidenceRefs=[f"skill:{skill['skill_id']}", f"run:{solution.runId}"], solveStatus=solution.status, solveTimeSec=float((solution.solverMeta or {}).get("timeSec") or 0) or None, ) def _call_skill(self, skill: dict[str, Any], problem: dict) -> SchedulingSolution: ep = skill.get("endpoint") or "" if ep.startswith("local://"): from server.integrations.algo_skill_stub import solve_problem return solve_problem(problem) url = ep.rstrip("/") + "/schedule" headers = {"Content-Type": "application/json"} if skill.get("auth"): headers["Authorization"] = f"Bearer {skill['auth']}" timeout = float(skill.get("timeout_sec") or 60) try: with httpx.Client(timeout=timeout) as client: resp = client.post(url, json=problem, headers=headers) resp.raise_for_status() return SchedulingSolution(**resp.json()) except Exception as exc: raise ValueError(f"外部算法 skill「{skill.get('name')}」调用失败:{exc}") from exc def run_external_flex(store, *, skill_id: str | None = None, order_ids: list[int] | None = None, actor: str = "planner") -> dict[str, Any]: """供 workflow 直接调用的柔性外部排产(不经固定轨 ScheduleResult 路径)。""" from server.agent_core.skills import get_skills from server.timeutil import add_minutes, fmt_dt, today0 reg = get_skills() skill = reg.get_enabled(skill_id, track="flex") or reg.get_enabled(skill_id) or reg.get_enabled() if not skill: raise ValueError("没有可用的外部算法 skill") problem = world_to_flex_problem( store.data, order_ids=order_ids, start_time=fmt_dt(add_minutes(today0(), 24 * 60)), ) eng = ExternalEngine(skill_id=skill["skill_id"]) solution = eng._call_skill(skill, problem.model_dump()) summary = apply_flex_solution(store.data, solution, store.next_id, skill_id=skill["skill_id"]) store.save() return summary