aps-agent/server/aps_domain/guidance.py

332 lines
15 KiB
Python
Raw Permalink 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: 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": "按订单列出实际阻断项"},
{"id": "wizard", "label": "带我补齐并排产",
"command": "带我排一版", "priority": 15,
"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"),
},
}