104 lines
4.6 KiB
Python
104 lines
4.6 KiB
Python
|
|
# ============================================================
|
|||
|
|
# 外部算法引擎适配器(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
|