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
|