aps-agent/server/agent_core/algolib.py

497 lines
25 KiB
Python
Raw Normal View History

# ============================================================
# 内置算法库注册表(moduleId: core-algolib, 可重生 ✅)
# plan.md §9.5:算法收敛为带注册表的可重生资产(契约/黄金测试/适用边界);
# 与 core-skills(外部算法 SkillRegistry)区分——本表只登记**内置**算法,
# SkillRegistry 管外部可插拔算法(HTTP/local 端点)。
# 资产分类(矩阵 86 行口径,A-E 五类):
# A=启发式(调度规则/构造启发式) B=精确(CP-SAT/MILP)
# C=元启发(GA/LNS/SA/NSGA-II) D=ML(预测器/代理模型)
# E=集成(混合引擎/评估链:HYBRID、敏感性、鲁棒性、参数优化)
# 注:plan.md §9.5.1 的五类与任务矩阵 86 行分类略有差异,本表以
# 任务矩阵口径为准(元启发独立成 C,ML 归 D,E 为集成/评估链)。
# ============================================================
from __future__ import annotations
import importlib
import threading
import time
from collections.abc import Callable
from typing import Any
from pydantic import BaseModel, Field
# 类别定义:A-E 五类资产的用途与适用边界(查询/健康检查展示用)
CATEGORY_LABELS: dict[str, str] = {
"A": "启发式:调度规则/构造启发式(EDD/SPT/ATC/贪心插入),毫秒级,作初始解与兜底",
"B": "精确:CP-SAT/MILP 等精确或近似优化,秒~分钟级,规模与时限需匹配",
"C": "元启发:GA/LNS/SA/NSGA-II,非确定性算法需固定随机种子才可复现",
"D": "ML:预测器/代理模型(工时回归/交期概率/需求时序),需历史数据、输出带置信区间",
"E": "集成:混合引擎与评估链(RULE+CP HYBRID、敏感性分析、蒙特卡洛鲁棒性、参数优化)",
}
# 默认随机种子(注册表级):非确定性内置算法复现的统一种子源
DEFAULT_SEED = 20260802
# 健康检查历史保留条数
_HEALTH_HISTORY_KEEP = 20
class AlgorithmManifest(BaseModel):
"""算法注册表条目(plan.md §9.5.2 AlgorithmManifest 的内置实现形态)。
每个算法自描述:编排器/LLM 据此选型与调用;golden_tests 是重生验收。
"""
algo_id: str # 算法唯一 ID,如 "rule.delivery_first"
name: str # 展示名
category: str # A/B/C/D/E(对应 CATEGORY_LABELS)
description: str = "" # 一句话用途
scale_limit: str = "" # 适用规模声明,如 "<=50k 工单"
time_budget: str = "" # 典型时限,如 "5s~5min"
input_schema: dict[str, Any] = Field(default_factory=dict) # 输入契约(JSON Schema 形态)
output_schema: dict[str, Any] = Field(default_factory=dict) # 输出契约(含证据字段)
golden_tests: list[str] = Field(default_factory=list) # 黄金测试用例路径(重生验收)
deterministic: bool = True # 是否确定性(GA/SA 需固定种子)
random_seed: int | None = None # 随机种子(非确定性算法的复现锚点)
version: str = "1.0" # 语义化版本
regen_strategy: str = "manual" # 重生策略:llm / manual / hybrid
available: bool = True # 内置实现是否就绪(False=目录资产未落地)
entrypoint: str = "" # 取用入口:引擎名(RULE/CP/GA/HYBRID)、
# "ENGINE:STRATEGY" 或 "module.path:attr"
health_fn: str | Callable[[], dict[str, Any]] = "" # 可选健康探针(点路径或可调用对象)
def validate_manifest(self) -> list[str]:
"""元数据完整性校验,返回问题列表(空=通过)。"""
problems: list[str] = []
if not self.algo_id or not self.algo_id.strip():
problems.append("algo_id 缺失")
if not self.name or not self.name.strip():
problems.append("name 缺失")
if self.category not in CATEGORY_LABELS:
problems.append(f"category 非法:{self.category!r}(须为 A/B/C/D/E)")
if not isinstance(self.input_schema, dict) or not isinstance(self.output_schema, dict):
problems.append("input_schema/output_schema 必须是 dict")
if not isinstance(self.golden_tests, list):
problems.append("golden_tests 必须是 list")
if not isinstance(self.deterministic, bool):
problems.append("deterministic 必须是 bool")
if not self.version or not str(self.version).strip():
problems.append("version 缺失")
if self.regen_strategy not in ("llm", "manual", "hybrid"):
problems.append(f"regen_strategy 非法:{self.regen_strategy!r}")
if not self.deterministic and self.random_seed is None:
problems.append("非确定性算法必须声明 random_seed(复现锚点)")
return problems
def _resolve_health_fn(
spec: str | Callable[[], dict[str, Any]],
) -> Callable[[], dict[str, Any]] | None:
"""把 health_fn 规范解析为可调用对象(点路径或原对象)。"""
if callable(spec):
return spec
if isinstance(spec, str) and ":" in spec:
mod_path, attr = spec.rsplit(":", 1)
try:
fn = getattr(importlib.import_module(mod_path), attr)
return fn if callable(fn) else None
except (ImportError, AttributeError):
return None
return None
def _builtin_catalog() -> list[AlgorithmManifest]:
"""内置算法目录:只登记仓库内真实存在的实现(available=True),
未落地的规划资产(MILP/ML 预测器)以 available=False 登记,
健康检查如实报告未就绪,避免把目录当成果(LNS 已随 round-36 交付转为可用)。"""
rule_tests = ["tests/golden/test_planning.py", "tests/golden/test_schedule_wizard.py"]
cp_tests = ["tests/golden/test_cp_engine.py"]
kpi_out = {
"scheduleVersion": {"id": "int", "versionNo": "str", "status": "str"},
"productionOrders": "array<生产订单>",
"workOrders": "array<工单>",
"conflicts": "array<冲突>",
"kpi": {"tardiness": "float(小时)", "conflicts": "int",
"utilization": "float", "changeoverMin": "float"},
}
rule_in = {
"salesOrders": "array<订单>", "lines": "array<产线>",
"strategy": "RULE 策略模板", "planningHorizonDays": "int",
"constraints": "dict<bool 约束开关>",
}
items = [
# ---- A. 启发式(RULE 引擎各策略模板)----
AlgorithmManifest(
algo_id="rule.delivery_first", name="EDD 最早交期",
category="A", description="交期优先派工(单机 1||Lmax 最优,Jackson 定理)",
scale_limit="<=50k 工单", time_budget="毫秒级",
input_schema=rule_in, output_schema=kpi_out,
golden_tests=list(rule_tests), deterministic=True,
entrypoint="RULE:DELIVERY_FIRST", regen_strategy="manual",
),
AlgorithmManifest(
algo_id="rule.fifo", name="FIFO 先来先服务",
category="A", description="按订单下达时间排序",
scale_limit="<=50k 工单", time_budget="毫秒级",
input_schema=rule_in, output_schema=kpi_out,
golden_tests=list(rule_tests), deterministic=True,
entrypoint="RULE:FIFO", regen_strategy="manual",
),
AlgorithmManifest(
algo_id="rule.comprehensive", name="COMPREHENSIVE 综合策略",
category="A", description="等级权重+优先级+交期综合排序(默认派工策略)",
scale_limit="<=50k 工单", time_budget="毫秒级",
input_schema=rule_in, output_schema=kpi_out,
golden_tests=list(rule_tests), deterministic=True,
entrypoint="RULE:COMPREHENSIVE", regen_strategy="manual",
),
AlgorithmManifest(
algo_id="rule.capacity_balance", name="CAPACITY_BALANCE 产能均衡",
category="A", description="按等级+优先级+累计占用均衡选线",
scale_limit="<=50k 工单", time_budget="毫秒级",
input_schema=rule_in, output_schema=kpi_out,
golden_tests=list(rule_tests), deterministic=True,
entrypoint="RULE:CAPACITY_BALANCE", regen_strategy="manual",
),
AlgorithmManifest(
algo_id="rule.changeover_min", name="CHANGEOVER_MIN 换型最小化",
category="A", description="贪心最近邻最小化换型(SC-07)",
scale_limit="<=50k 工单", time_budget="毫秒级",
input_schema=rule_in, output_schema=kpi_out,
golden_tests=["tests/golden/test_changeover.py"], deterministic=True,
entrypoint="RULE:CHANGEOVER_MIN", regen_strategy="manual",
),
AlgorithmManifest(
algo_id="rule.cost_first", name="COST_FIRST 成本优先",
category="A", description="以换型耗时为成本代理排序(SC-07 备选)",
scale_limit="<=50k 工单", time_budget="毫秒级",
input_schema=rule_in, output_schema=kpi_out,
golden_tests=["tests/golden/test_changeover.py"], deterministic=True,
entrypoint="RULE:COST_FIRST", regen_strategy="manual",
),
AlgorithmManifest(
algo_id="rule.campaign", name="CAMPAIGN 战役合并",
category="A", description="同产品交期窗口战役合并,按换型最小化排战役序(SC-08)",
scale_limit="<=50k 工单", time_budget="毫秒级",
input_schema=rule_in, output_schema=kpi_out,
golden_tests=["tests/golden/test_campaign.py"], deterministic=True,
entrypoint="RULE:CAMPAIGN", regen_strategy="manual",
),
# ---- B. 精确 ----
AlgorithmManifest(
algo_id="cp_sat", name="CP-SAT 约束规划",
category="B", description="OR-Tools CP-SAT 详排求优(IntervalVar+NoOverlap+Cumulative)",
scale_limit="<=50k 工单", time_budget="5s~5min",
input_schema={**rule_in, "timeLimitSeconds": "float(秒,anytime)"},
output_schema=kpi_out,
golden_tests=list(cp_tests), deterministic=True,
entrypoint="CP", regen_strategy="hybrid",
),
AlgorithmManifest(
algo_id="milp", name="MILP 混合整数规划",
category="B", description="S1 集团产能平衡与小规模基准校核(析取建模,Manne 1960)",
scale_limit="<=2k 工单", time_budget="分钟级",
input_schema=rule_in, output_schema=kpi_out,
golden_tests=[], deterministic=True,
entrypoint="", available=False, regen_strategy="manual",
),
# ---- C. 元启发 ----
AlgorithmManifest(
algo_id="ga", name="遗传算法 GA",
category="C", description="种群/代数演化搜索近优解",
scale_limit="<=10k 工单", time_budget="秒~分钟级",
input_schema={**rule_in, "population": "int", "generations": "int"},
output_schema=kpi_out,
golden_tests=["tests/golden/test_ga_engine.py"], deterministic=False,
random_seed=DEFAULT_SEED, entrypoint="GA", regen_strategy="hybrid",
),
AlgorithmManifest(
algo_id="lns", name="LNS/ALNS 大邻域搜索",
category="C", description="插单局部修复:固定窗口最小扰动,超阈值升级全量重排(§9.11,round-36 交付)",
scale_limit="<=50k 工单", time_budget="<30s",
input_schema={**rule_in, "frozenWindowHours": "float", "maxAffectedOrders": "int", "disturbanceTolerance": "float"},
output_schema=kpi_out,
golden_tests=["tests/golden/test_rush_lns.py"], deterministic=False,
random_seed=DEFAULT_SEED, entrypoint="server.aps_domain.lns:lns_local_repair",
available=True, regen_strategy="hybrid",
),
AlgorithmManifest(
algo_id="nsga2", name="NSGA-II 多目标遗传",
category="C", description="多目标帕累托解集:快速非支配排序+拥挤度+精英保留,3 目标(总延迟/容量冲突/负载均衡)(§9.7,round-40 交付)",
scale_limit="<=10k 工单", time_budget="分钟级",
input_schema={**rule_in, "objectives": "array<目标键>", "population": "int", "generations": "int"},
output_schema={**kpi_out, "paretoSet": "array<方案>"},
golden_tests=["tests/golden/test_nsga2_engine.py"], deterministic=False,
random_seed=DEFAULT_SEED, entrypoint="server.engines.nsga2_engine:solve_nsga2",
available=True, regen_strategy="hybrid",
),
# ---- D. ML(预测器/代理模型,当前目录资产,未落地实现)----
AlgorithmManifest(
algo_id="ml.worktime_regression", name="实际工时回归",
category="D", description="LightGBM/XGBoost 工时估计(§9.1a OR 输入参数估准)",
scale_limit="需历史报工数据", time_budget="训练级",
input_schema={"features": "dict", "history": "array<报工记录>"},
output_schema={"estimate": "float", "confidenceInterval": "[low, high]"},
golden_tests=[], deterministic=False,
random_seed=DEFAULT_SEED, entrypoint="", available=False, regen_strategy="llm",
),
AlgorithmManifest(
algo_id="ml.due_date_survival", name="交期达成概率",
category="D", description="生存分析:订单交期达成概率",
scale_limit="需历史订单数据", time_budget="训练级",
input_schema={"order": "dict", "history": "array<历史订单>"},
output_schema={"deliveryProbability": "float", "confidenceInterval": "[low, high]"},
golden_tests=[], deterministic=False,
random_seed=DEFAULT_SEED, entrypoint="", available=False, regen_strategy="llm",
),
# ---- E. 集成(混合引擎与评估链)----
AlgorithmManifest(
algo_id="hybrid", name="HYBRID 规则+CP 混合",
category="E", description="RULE 热启动构造初始解 + CP-SAT 局部改良",
scale_limit="<=50k 工单", time_budget="5s~5min",
input_schema={**rule_in, "timeLimitSeconds": "float(秒,anytime)"},
output_schema=kpi_out,
golden_tests=list(cp_tests), deterministic=False,
random_seed=DEFAULT_SEED, entrypoint="HYBRID", regen_strategy="hybrid",
),
AlgorithmManifest(
algo_id="eval.sensitivity_tornado", name="敏感性 Tornado 分析",
category="E", description="OAT 单因子扰动排序(SC-06,跑 Explore 沙盒)",
scale_limit="按需", time_budget="秒级/因子",
input_schema={"world": "dict", "strategy": "str"},
output_schema={"rows": "array<因子摆幅>", "monteCarlo": "dict", "markdown": "str"},
golden_tests=["tests/golden/test_sensitivity.py"], deterministic=True,
entrypoint="server.aps_domain.sensitivity:run_sensitivity",
regen_strategy="manual",
),
AlgorithmManifest(
algo_id="eval.monte_carlo_robustness", name="蒙特卡洛鲁棒性",
category="E", description="固定种子情景仿真,鲁棒性评分(§9.9)",
scale_limit="按需", time_budget="后台批量",
input_schema={"world": "dict", "trials": "int", "seed": "int"},
output_schema={"robustness": "float", "distribution": "dict", "seed": "int"},
golden_tests=["tests/golden/test_sensitivity.py"], deterministic=True,
random_seed=DEFAULT_SEED,
entrypoint="server.aps_domain.robustness:run_monte_carlo",
regen_strategy="manual",
),
AlgorithmManifest(
algo_id="opt.parameter_optimizer", name="参数优化闭环",
category="E", description="回放→灰度→验证→退化自动回滚(§9.8)",
scale_limit="按需", time_budget="版本级",
input_schema={"world": "dict", "candidates": "array<参数字典>"},
output_schema={"experiments": "array<实验记录>", "summary": "dict"},
golden_tests=["tests/golden/test_param_opt.py"], deterministic=True,
random_seed=DEFAULT_SEED,
entrypoint="server.agent_core.param_opt:ParameterOptimizer",
regen_strategy="manual",
),
]
return items
class AlgorithmRegistry:
"""内置算法注册表(内存态,无文件持久化——内置资产随代码版本演进)。
与 core-skills 的 SkillRegistry 区分:SkillRegistry 管**外部**算法
(manifest.json + HTTP 端点),本注册表管**内置**算法(引擎/策略/评估链)。
"""
def __init__(self, seed: int = DEFAULT_SEED) -> None:
self._lock = threading.Lock()
self._seed = int(seed)
self._algos: dict[str, AlgorithmManifest] = {}
self.health_history: dict[str, list[dict[str, Any]]] = {}
for m in _builtin_catalog():
self._algos[m.algo_id] = m
# ---------------- 注册 / 注销 ----------------
def register(self, manifest: AlgorithmManifest | dict[str, Any]) -> dict[str, Any]:
"""注册或更新(upsert)一个内置算法。元数据不完整直接拒绝。"""
m = manifest if isinstance(manifest, AlgorithmManifest) else AlgorithmManifest(**manifest)
problems = m.validate_manifest()
if problems:
raise ValueError("算法元数据不完整:" + "; ".join(problems))
with self._lock:
self._algos[m.algo_id] = m
return m.model_dump()
def unregister(self, algo_id: str) -> bool:
"""注销算法(内置资产不建议注销;测试/热替换用)。"""
with self._lock:
return self._algos.pop(algo_id, None) is not None
# ---------------- 查询 ----------------
def get(self, algo_id: str) -> dict[str, Any] | None:
m = self._algos.get(algo_id)
return m.model_dump() if m else None
def list(self) -> list[dict[str, Any]]:
return [m.model_dump() for m in self._algos.values()]
def query(
self,
*,
category: str | None = None,
available: bool | None = None,
keywords: str = "",
max_time_budget: str | None = None,
) -> list[dict[str, Any]]:
"""按类别/可用性/关键词/时限声明查询(编排器选型入口)。"""
kw = keywords.strip().lower()
out: list[dict[str, Any]] = []
for m in self._algos.values():
if category and m.category != category:
continue
if available is not None and m.available != available:
continue
if kw and kw not in (m.name + m.description + m.algo_id).lower():
continue
if max_time_budget and not self._budget_le(m.time_budget, max_time_budget):
continue
out.append(m.model_dump())
out.sort(key=lambda x: (x["category"], x["algo_id"]))
return out
def categories(self) -> dict[str, str]:
return dict(CATEGORY_LABELS)
def version(self, algo_id: str) -> dict[str, Any] | None:
"""版本查询:返回 {algo_id, version, regen_strategy, available}。"""
m = self._algos.get(algo_id)
if not m:
return None
return {
"algo_id": m.algo_id,
"version": m.version,
"regen_strategy": m.regen_strategy,
"available": m.available,
}
def next_seed(self, algo_id: str) -> int:
"""随机种子服务:确定性算法返回声明的种子(无则 None 语义用 0);
非确定性算法返回按 algo_id 稳定的种子(同注册表种子源可复现)。"""
m = self._algos.get(algo_id)
if not m:
return self._seed
if m.deterministic:
return m.random_seed if m.random_seed is not None else 0
return m.random_seed if m.random_seed is not None else (self._seed + sum(map(ord, algo_id)))
# ---------------- 健康检查 ----------------
def health(self, algo_id: str | None = None) -> list[dict[str, Any]]:
"""健康检查:元数据完整性 + 就绪状态 + 可选探针;记录历史(最近 N 条)。"""
targets = [self._algos[algo_id]] if algo_id and algo_id in self._algos else ([] if algo_id else list(self._algos.values()))
out: list[dict[str, Any]] = []
for m in targets:
status = self._probe(m)
record = {"ts": time.strftime("%Y-%m-%d %H:%M:%S"), **status}
hist = self.health_history.setdefault(m.algo_id, [])
hist.append(record)
del hist[:-_HEALTH_HISTORY_KEEP]
out.append({
"algo_id": m.algo_id,
"name": m.name,
"category": m.category,
"version": m.version,
"available": m.available,
**status,
})
return out
def history(self, algo_id: str) -> list[dict[str, Any]]:
return list(self.health_history.get(algo_id) or [])
def _probe(self, m: AlgorithmManifest) -> dict[str, Any]:
"""单算法探针:元数据→可用性→入口→自定义探针,逐级短路报告。"""
started = time.time()
problems = m.validate_manifest()
if problems:
return {"ok": False, "latencyMs": 0, "detail": "元数据不完整: " + "; ".join(problems)}
if not m.available:
return {"ok": False, "latencyMs": 0, "detail": "目录资产未落地(available=False)"}
detail = "builtin"
if m.entrypoint:
ok, err = self._check_entrypoint(m.entrypoint)
if not ok:
return {"ok": False, "latencyMs": 0, "detail": f"入口不可达: {err}"}
detail = f"entrypoint ok ({m.entrypoint})"
if m.health_fn:
fn = _resolve_health_fn(m.health_fn)
if fn is None:
return {"ok": False, "latencyMs": 0, "detail": "health_fn 不可解析"}
try:
r = fn() or {}
if not isinstance(r, dict) or "ok" not in r:
return {"ok": False, "latencyMs": 0, "detail": "health_fn 返回格式非法"}
latency = int((time.time() - started) * 1000)
return {"ok": bool(r["ok"]), "latencyMs": latency,
"detail": str(r.get("detail") or detail)}
except Exception as exc: # noqa: BLE001 - 健康探针必须兜底任意异常(异常即不健康)
return {"ok": False, "latencyMs": 0, "detail": f"health_fn 异常: {exc}"}
return {"ok": True, "latencyMs": int((time.time() - started) * 1000), "detail": detail}
@staticmethod
def _check_entrypoint(entrypoint: str) -> tuple[bool, str]:
"""入口可达性:引擎名 / ENGINE:STRATEGY / module.path:attr。"""
if ":" not in entrypoint:
return True, "" # 纯引擎名(RULE/CP/GA/HYBRID)由 get_engine 工厂保证
if entrypoint.startswith("RULE:"):
from server.engines import get_engine
try:
get_engine("RULE")
return True, ""
except (ImportError, AttributeError, RuntimeError, ValueError, TypeError) as exc:
return False, str(exc)
mod_path, attr = entrypoint.rsplit(":", 1)
try:
getattr(importlib.import_module(mod_path), attr)
return True, ""
except (ImportError, AttributeError) as exc:
return False, str(exc)
@staticmethod
def _budget_le(declared: str, limit: str) -> bool:
"""时限声明比较(粗粒度:解析首段数值,如 "5s~5min" → 5min)。"""
def parse(spec: str) -> float:
text = spec.strip().lower()
if not text:
return 0.0
num = ""
for ch in text:
if ch.isdigit() or ch == ".":
num += ch
elif num:
break
try:
val = float(num)
except ValueError:
return 0.0
if "ms" in text:
return val / 1000.0
if "s" in text:
return val
if "min" in text:
return val * 60
if "h" in text:
return val * 3600
return val
return parse(declared) <= parse(limit) if parse(limit) > 0 else True
_registry: AlgorithmRegistry | None = None
def get_algolib() -> AlgorithmRegistry:
"""单例获取(与 get_skills 对齐)。"""
global _registry
if _registry is None:
_registry = AlgorithmRegistry()
return _registry
def reset_algolib_registry() -> None:
"""测试用:丢弃单例,下次按默认目录重建。"""
global _registry
_registry = None