aps-agent/server/engines/external_engine.py

104 lines
4.6 KiB
Python
Raw Normal View History

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