aps-agent/server/aps_domain/guidance.py

329 lines
15 KiB
Python
Raw Normal View History

# ============================================================
# 主动引导(moduleId: domain-guidance, 可重生 ✅)
# AG-07:按世界状态给出下一步建议(空态 / 冲突高 / 知识未命中)
# ============================================================
from __future__ import annotations
from typing import Any
World = dict[str, Any]
def build_guidance(world: World, *, context: str | None = None) -> dict[str, Any]:
"""根据世界状态产出信号与可点击下一步建议。
context: 可选强化场景(knowledge_miss / high_conflict / empty_schedule)
"""
signals: list[dict[str, Any]] = []
suggestions: list[dict[str, Any]] = []
versions = world.get("scheduleVersions") or []
flex_versions = world.get("flexScheduleVersions") or []
conflicts = [c for c in (world.get("conflicts") or []) if c.get("status") != "RESOLVED"]
flex_conflicts = [c for c in (world.get("flexConflicts") or []) if c.get("status") != "RESOLVED"]
open_conflicts = len(conflicts) + len(flex_conflicts)
latest = versions[-1] if versions else None
latest_flex = flex_versions[-1] if flex_versions else None
current = latest_flex or latest
draft = current and current.get("status") == "DRAFT"
flex_orders = world.get("flexOrders") or []
# 没有现成方案时,排产齐备度优先于“甘特为空”。零订单或零可排订单
# 只能引导补数,不能给用户任何试排入口。
if not current:
from server.aps_domain.readiness import check_readiness
readiness = check_readiness(world)
readiness_summary = readiness.get("summary") or {}
if not readiness_summary.get("total") or not readiness_summary.get("ready"):
guide = scheduling_data_guide(world)
guide["context"] = context or guide.get("context")
return guide
from server.importers.workbook_profiles import has_adoption_flow
if has_adoption_flow((world.get("planningContext") or {}).get("sourceProfile")):
from server.aps_domain.readiness import check_readiness
readiness = check_readiness(world)
pending = readiness.get("summary") or {}
if pending.get("blocked", 0) or readiness.get("globalIssues"):
return {
"signals": [{"id": "master_review", "severity": "high",
"title": "排产资料仍有待核对信息",
"reason": "资料已采用,请先处理人员、在制或供料问题,再生成可靠方案。"}],
"suggestions": [{"id": "review_missing", "label": "查看需要补充的信息",
"command": "排产还缺什么", "priority": 20,
"reason": "按订单列出实际阻断项"}],
"hint": "资料已采用,请先核对影响排产的问题;系统会保留你在主数据中的修改。",
"context": context,
"stats": {"scheduleVersions": len(versions), "flexScheduleVersions": len(flex_versions),
"blockedOrders": int(pending.get("blocked") or 0), "openConflicts": open_conflicts},
}
# ---- 信号检测 ----
if not versions and not flex_versions:
signals.append({
"id": "no_version", "severity": "high",
"title": "还没有排产方案",
"reason": "甘特为空,需先试排生成草稿。",
})
suggestions.append({
"id": "run_delivery", "label": "生成交期优先试排方案",
"command": "生成一版交期优先试排方案", "priority": 10,
"reason": "无版本时空态首选",
})
suggestions.append({
"id": "scenario_compare", "label": "多策略方案对比",
"command": "多策略方案对比", "priority": 8,
"reason": "不确定策略时先沙盒对比",
})
conflict_n = open_conflicts
if latest:
conflict_n = max(conflict_n, int(latest.get("conflictCount") or 0))
if latest_flex:
conflict_n = max(conflict_n, int(latest_flex.get("conflictCount") or 0))
if conflict_n >= 3 or context == "high_conflict":
signals.append({
"id": "high_conflict", "severity": "high" if conflict_n >= 5 else "medium",
"title": f"冲突偏高(约 {conflict_n} 项)",
"reason": "建议先看冲突中心,或换策略对比后再发布。",
})
suggestions.append({
"id": "view_conflicts", "label": "查看冲突",
"command": "查看冲突", "priority": 20,
"reason": "冲突高时优先定位",
})
suggestions.append({
"id": "compare_strategies", "label": "多策略方案对比",
"command": "多策略方案对比", "priority": 18,
"reason": "用沙盒评估缓解冲突的策略",
})
if current:
suggestions.extend([
{"id": "view_schedule", "label": "查看排产结果",
"command": "查看排产结果",
"priority": 15, "reason": "先检查已经生成的方案"},
{"id": "export_schedule", "label": "导出排产表",
"command": "生成排产方案报告", "priority": 7,
"reason": "导出当前方案供核对"},
])
if draft and current:
trial = current.get("trialOnly") is True or current.get("productionReady") is False
signals.append({
"id": "draft_ready", "severity": "low",
"title": f"已生成方案 {current.get('versionNo') or ''}",
"reason": ("这是试排方案,尚未下发到车间。请先核对订单、设备和交期。"
if trial else "方案已生成。请先检查交期和安排,再决定是否发布。"),
})
if not trial and conflict_n == 0:
suggestions.append({
"id": "publish", "label": "确认发布方案",
"command": "发布当前排产版本", "priority": 6,
"reason": "草稿已生成,发布仍需确认",
})
elif current:
signals.append({
"id": "schedule_exists", "severity": "low",
"title": f"已有排产方案 {current.get('versionNo') or ''}",
"reason": "先查看现有安排;如有插单、缺料或设备变化,再调整方案。",
})
if flex_orders and not flex_versions and not versions:
signals.append({
"id": "no_flex", "severity": "medium",
"title": "订单已准备好,等待生成方案",
"reason": "先核对订单和设备,再生成试排方案。",
})
suggestions.append({
"id": "flex_run", "label": "生成排产方案",
"command": "生成一版柔性排产方案", "priority": 12,
"reason": "柔性轨空态",
})
if context == "knowledge_miss":
signals.append({
"id": "knowledge_miss", "severity": "medium",
"title": "知识未命中",
"reason": "可换说法,或浏览已有知识资产。",
})
suggestions.append({
"id": "kb_list", "label": "查看知识库清单",
"command": "知识库", "priority": 25,
"reason": "未命中时的下一步",
})
suggestions.append({
"id": "kb_machining", "label": "机加工工艺怎么生成",
"command": "机加工工艺怎么生成", "priority": 24,
"reason": "机械加工工艺路线总则",
})
suggestions.append({
"id": "kb_sop", "label": "问主数据录入顺序",
"command": "主数据录入顺序有什么规定", "priority": 22,
"reason": "常见 SOP 样例",
})
# 去重(按 command 保留最高 priority)
by_cmd: dict[str, dict[str, Any]] = {}
for s in suggestions:
prev = by_cmd.get(s["command"])
if prev is None or s["priority"] > prev["priority"]:
by_cmd[s["command"]] = s
ranked = sorted(by_cmd.values(), key=lambda x: -x["priority"])[:5]
if not ranked:
ranked = [{
"id": "help", "label": "查看可用操作",
"command": "帮助", "priority": 1,
"reason": "默认兜底",
}]
hint = "根据当前状态,建议下一步:"
if signals:
hint = signals[0]["reason"]
return {
"signals": signals,
"suggestions": ranked,
"hint": hint,
"context": context,
"stats": {
"scheduleVersions": len(versions),
"flexScheduleVersions": len(flex_versions),
"openConflicts": open_conflicts,
"conflictScore": conflict_n,
},
}
def guidance_ui_block(world: World, *, context: str | None = None) -> dict[str, Any]:
"""构造 guidance UI 块 props(由调用方包进 UIBlock)。"""
return build_guidance(world, context=context)
def _guide_step(step_id: str, title: str, detail: str, how: str,
actions: list[dict[str, str]]) -> dict[str, Any]:
return {
"id": step_id,
"title": title,
"detail": detail,
"how": how,
"actions": actions,
}
def scheduling_data_guide(world: World) -> dict[str, Any]:
"""排产缺数据的傻瓜式引导:按顺序列出缺什么、为什么、怎么补。
输出与 guidance 块同构(steps + suggestions + stats),前端可按步骤
一键回填口令,非专业人员跟着做即可。
"""
from server.aps_domain.readiness import check_readiness
report = check_readiness(world)
s = report["summary"]
orders = [o for o in (world.get("flexOrders") or [])
if (o.get("status") or "") not in ("DONE", "CANCELLED")]
materials = world.get("flexMaterials") or []
routings = world.get("flexRoutings") or []
equipment = [e for e in (world.get("flexEquipment") or []) if e.get("status") == "RUNNING"]
calendar = world.get("flexCalendar") or []
issue_types = {i["type"] for r in report["orders"] for i in r["issues"]}
steps: list[dict[str, Any]] = []
if not orders:
steps.append(_guide_step(
"orders", "先告诉我排哪些单",
"现在还没有待排订单。",
"直接发一张订单表(Excel/CSV),或在对话里说「新建订单 产品A 100件 8月20日交」;也可以让我带你一步一步录。",
[{"label": "带我录订单并排产", "command": "带我排一版"}],
))
if not materials or "NO_PRODUCT" in issue_types:
missing_n = sum(1 for r in report["orders"]
if any(i["type"] == "NO_PRODUCT" for i in r["issues"]))
steps.append(_guide_step(
"products", "把要做的产品告诉系统",
"还没有产品/材料档案" if not materials else f"有 {missing_n} 张订单的产品查不到档案。",
"发一份产品/材料名录,或说「帮我分析项目文件数据」,我自动从表格里补。",
[{"label": "带我排一版", "command": "带我排一版"}],
))
if not routings or "NO_ROUTING" in issue_types:
missing_n = sum(1 for r in report["orders"]
if any(i["type"] == "NO_ROUTING" for i in r["issues"]))
steps.append(_guide_step(
"routing", "产品怎么做还没定",
"还没有工艺路线" if not routings else f"有 {missing_n} 张订单的产品没有工艺路线。",
"说「带我排一版」,我会先推荐常用工艺模板,你挑一个或改一下就行。",
[{"label": "让我选工艺模板", "command": "带我排一版"}],
))
if not equipment or "NO_CAPABLE_EQUIPMENT" in issue_types:
missing_n = sum(1 for r in report["orders"]
if any(i["type"] == "NO_CAPABLE_EQUIPMENT" for i in r["issues"]))
steps.append(_guide_step(
"equipment", "还缺能干活的设备/工序能力",
"没有运行中的设备" if not equipment else f"有 {missing_n} 张订单的工序找不到能做它的设备。",
"发设备台账,或在主数据页给设备勾上会做的工序;拿不准就说「带我排一版」,我告诉你缺在哪。",
[{"label": "带我排一版", "command": "带我排一版"}],
))
if not calendar:
steps.append(_guide_step(
"calendar", "还没告诉系统几点上班",
"没有班次日历,系统不知道每天几点开工、一周开几天。",
"说「默认日历」用常见班制(周一至周五 8:00-17:00),或告诉我班制,比如「两班 8点到23点 周一到周六」。",
[{"label": "带我排一版", "command": "带我排一版"}],
))
if s.get("timePending"):
steps.append(_guide_step(
"time", "有几道工序工时还没填",
f"还有 {s['timePending']} 道工序没有单件工时,硬排会失真。",
"说「带我排一版」,我会一条一条问你「这步每件多少分钟」,你回数字就行。",
[{"label": "逐条补工时", "command": "带我排一版"}],
))
if "MATERIAL_GAP" in issue_types:
gap_n = sum(1 for r in report["orders"]
if any(i["type"] == "MATERIAL_GAP" for i in r["issues"]))
steps.append(_guide_step(
"material", "部分物料可能不够",
f"有 {gap_n} 张订单的 BOM 物料库存/在途不足(提醒,不影响先试排)。",
"补库存表或采购到货信息;不补也能先试排,结果里会标出来。",
[{"label": "先试排看看", "command": "直接排一版"}],
))
suggestions = [{
"id": "wizard", "label": "带我补齐并排产", "command": "带我排一版",
"priority": 30, "reason": "一步步跟着补最稳",
}]
if not steps:
suggestions = [{
"id": "run", "label": "直接排一版", "command": "直接排一版",
"priority": 30, "reason": "数据基本齐了",
}, {
"id": "recheck", "label": "再看还缺什么", "command": "排产还缺什么",
"priority": 25, "reason": "需要时可复查",
}]
return {
"mode": "ready" if not steps else "data-missing",
"hint": ("数据基本齐了,可以直接试排。" if not steps
else f"要开排还差 {len(steps)} 样东西,我按顺序列好了,补一样少一样。"),
"signals": [{
"id": "data-missing", "severity": "high",
"title": f"还差 {len(steps)} 样数据",
"reason": "补齐后才能排出可信方案。",
}] if steps else [],
"steps": steps,
"suggestions": suggestions,
"context": "data-missing" if steps else "ready",
"stats": {
"orders": len(orders), "materials": len(materials),
"routings": len(routings), "equipment": len(equipment),
"calendar": len(calendar),
"ready": s.get("ready"), "blocked": s.get("blocked"),
"timePending": s.get("timePending"),
},
}