aps-agent/server/engines/external_engine.py

104 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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