# ============================================================ # 主动引导(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 draft = latest and latest.get("status") == "DRAFT" flex_orders = world.get("flexOrders") or [] # ---- 信号检测 ---- if not 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 draft and latest: signals.append({ "id": "draft_ready", "severity": "low", "title": f"草稿 {latest.get('versionNo')} 待发布", "reason": "确认无重大冲突后可发布为执行基准。", }) suggestions.append({ "id": "publish", "label": "发布当前排产版本", "command": "发布当前排产版本", "priority": 6, "reason": "草稿就绪", }) if flex_orders and not flex_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": "带我排一版"}, {"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": "数据基本齐了", }] suggestions.append({ "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"), }, }