366 lines
28 KiB
Python
366 lines
28 KiB
Python
# ============================================================
|
||
# 排产工作流编排(moduleId: domain-workflow, 可重生 ✅)
|
||
# 职责:意图 → 动作 → AgentReply(文本 + 视口命令 + UI 块)
|
||
# 权力路由:P0/P1 直接执行;P2(发布/重置)经门禁出确认卡(§3.3)
|
||
# ============================================================
|
||
from __future__ import annotations # 前向类型引用
|
||
|
||
from typing import Any # 类型标注
|
||
|
||
from server.agent_core import harness # 门禁 v1
|
||
from server.agent_core.audit import write_audit # 审计写入
|
||
from server.contracts import ( # 跨层契约
|
||
AgentReply, IntentResult, ScheduleResult, ViewportCommand,
|
||
)
|
||
from server.engines import get_engine # 引擎工厂
|
||
from server.engines.base import EngineParams # 引擎入参
|
||
from server.state.checkpoints import get_checkpoints # 成对快照仓(§4.3)
|
||
from server.state.store import WorldStore # 世界状态存储
|
||
from server.timeutil import add_minutes, fmt_date, today0 # 日期工具
|
||
from server.aps_domain.scenario import compare_scenarios # 方案对比(Explore 沙盒 §9.7)
|
||
from server.aps_domain.views import world_summary # KPI 摘要(query.kpi 用)
|
||
from server.aps_domain.reports import build_report # 报告生成 v1(§9.10)
|
||
from server.knowledge import get_knowledge, get_preferences # 知识库与偏好仓(M3 §8)
|
||
from server.knowledge.retrieval import search as kn_search # 混合检索(带出处 §8.2)
|
||
from server.contracts import UIBlock # 报告/证据块组装
|
||
|
||
# 帮助文案:能力清单(help 意图与拒识兜底共用)
|
||
_HELP = "\n".join([
|
||
"可执行的排产能力:",
|
||
"· 生成排产草案:试排一版交期优先 / 产能均衡重排 / 用规则引擎试排",
|
||
"· 方案评估:多策略方案对比(沙盒运行,不影响当前版本)",
|
||
"· 切到负荷热力图 / 交期承诺看板 / 甘特图",
|
||
"· 只看VIP订单 / 只看海尔智家的订单",
|
||
"· 聚焦产线A / 定位 L003",
|
||
"· 标出超期订单 / 查看冲突 / 按小时看 / 重置视图",
|
||
"· 建一个检查点 / 回滚到上一个检查点(需确认)",
|
||
"· 换线有什么规定 / @知识:换线标准SOP(知识库带出处)",
|
||
"· 生成日报 / 生成版本对比报告(可下载)",
|
||
"· 查看当前KPI / 发布当前排产版本(需确认)/ 重置数据(需确认)",
|
||
])
|
||
|
||
# 策略模板的中文名(回复文案用)
|
||
_STRATEGY_CN = {"DELIVERY_FIRST": "交期优先", "CAPACITY_BALANCE": "产能均衡",
|
||
"COST_FIRST": "成本最优", "FIFO": "先进先出", "COMPREHENSIVE": "综合优化"}
|
||
|
||
|
||
def _run_schedule(store: WorldStore, intent: IntentResult, actor: str) -> AgentReply:
|
||
"""执行试排(P1:写草稿版本 + 落盘;发布另走 P2 门禁)。"""
|
||
# 偏好个性化(§8.3 第一层):用户没点名策略时用其历史最常用策略
|
||
explicit = intent.params.get("strategy") # 用户明确点名的策略(可能为空)
|
||
pref_note = "" # 个性化说明(透明可解释)
|
||
if explicit: # 点名 → 直接用
|
||
strategy = explicit
|
||
else: # 未点名 → 查偏好仓
|
||
strategy, scores = get_preferences().preferred_strategy(default="COMPREHENSIVE")
|
||
if scores: # 有历史信号才标注(避免误导)
|
||
pref_note = f"\n(按你的使用偏好选了{_STRATEGY_CN.get(strategy, strategy)},历史使用 {scores.get(strategy, 0)} 次;点名策略可覆盖)"
|
||
params = EngineParams( # 组装引擎入参
|
||
orderIds=[], # M1:全部待排订单
|
||
engineType=intent.params.get("engine", "HYBRID"), # 引擎类型(M1 由 RULE 代跑)
|
||
strategyTemplate=strategy, # 策略模板(点名或偏好)
|
||
planningHorizonDays=14, # 展望期
|
||
startDate=fmt_date(add_minutes(today0(), 24 * 60)), # 明天开排
|
||
)
|
||
engine = get_engine(params.engineType) # 取引擎(M1 恒为 RULE)
|
||
result: ScheduleResult = engine.solve(store.data, params, store.next_id) # 求解(写内存世界)
|
||
# 审计:算法运行留痕(§3.6.2 算法运行类:引擎/策略/计数可复算)
|
||
write_audit(store.data, store.next_id, actor=actor, category="ALGO_RUN", action="schedule.run",
|
||
target={"type": "SCHEDULE_VERSION", "id": result.versionId},
|
||
power="P1", rationale={"strategy": params.strategyTemplate, "engine": params.engineType,
|
||
"evidence": result.evidenceRefs})
|
||
store.save() # 试排结果落盘(草稿版本也持久化)
|
||
get_preferences().record(params.strategyTemplate, source="schedule.run", actor=actor) # 偏好信号沉淀(§8.3)
|
||
engine_note = "" # 引擎诚实标注(M1 CP/GA 由 RULE 代跑)
|
||
if params.engineType != "RULE": # 请求了未落地引擎
|
||
engine_note = f"\n(注:{params.engineType} 求解器将在 M5 接入,本次由规则引擎代跑)"
|
||
text = (f"排产完成 ✅ 版本 {result.versionNo}({_STRATEGY_CN.get(result.strategy, result.strategy)})\n"
|
||
f"生产订单 {result.poCount} 个 / 工单 {result.woCount} 个\n"
|
||
f"冲突 {result.conflictCount} 项 · 总延迟 {round(result.totalTardiness)}h · "
|
||
f"平均利用率 {round(result.avgUtilization * 100)}%{engine_note}{pref_note}\n"
|
||
f"右侧甘特已刷新为该版本。执行“发布当前排产版本”可进入 P2 发布确认。") # 回复正文(含下一步引导)
|
||
return AgentReply(text=text, commands=[ # 联动命令:切甘特 + 重拉世界数据
|
||
ViewportCommand(cmd="viewport.mode", params={"mode": "gantt"}, issuedBy="LLM"),
|
||
ViewportCommand(cmd="world.refresh", issuedBy="SYSTEM"),
|
||
])
|
||
|
||
|
||
def _stage_publish(store: WorldStore, session_id: str, actor: str) -> AgentReply:
|
||
"""把“发布版本”压入门禁(P2:出确认卡,不立即执行)。"""
|
||
versions = store.data["scheduleVersions"] # 版本表
|
||
if not versions: # 无版本可发布
|
||
return AgentReply(text="当前没有可发布的排产版本。请先生成排产草案,例如:试排一版交期优先。")
|
||
v = versions[-1] # 最新版本
|
||
if v["status"] == "PUBLISHED": # 已发布防重
|
||
return AgentReply(text=f"版本 {v['versionNo']} 已是发布状态,无需重复发布。")
|
||
block = harness.stage_confirmation( # 生成确认卡并登记待确认队列
|
||
session_id, "schedule.publish", {"versionId": v["id"]},
|
||
title=f"发布排产版本 {v['versionNo']}",
|
||
summary_lines=[ # 影响面说明(计划员据此决策)
|
||
f"生产订单 {v['poCount']} 个 / 工单 {v['woCount']} 个将转入执行准备",
|
||
f"未解决冲突 {v['conflictCount']} 项 · 总延迟 {round(v['totalTardiness'])}h",
|
||
"发布后该版本成为执行基准(P2 写主干世界状态)",
|
||
])
|
||
# 审计:门禁出卡事件(GATE 类)
|
||
write_audit(store.data, store.next_id, actor=actor, category="GATE", action="schedule.publish.stage",
|
||
target={"type": "SCHEDULE_VERSION", "id": v["id"]}, power="P2",
|
||
rationale={"confirmId": block.props["confirmId"]})
|
||
store.save() # 审计落盘
|
||
return AgentReply(text=f"发布版本 {v['versionNo']} 属于 P2 写操作,需要你确认(见下方确认卡)。", blocks=[block])
|
||
|
||
|
||
def _stage_reset(store: WorldStore, session_id: str, actor: str) -> AgentReply:
|
||
"""把“重置数据”压入门禁(P2:破坏性操作必须确认)。"""
|
||
block = harness.stage_confirmation( # 生成确认卡
|
||
session_id, "data.reset", {},
|
||
title="清空并重置演示数据",
|
||
summary_lines=["将删除全部排产版本/工单/审计并重新播种(不可恢复)"])
|
||
return AgentReply(text="重置数据是破坏性操作(P2),需要你确认。", blocks=[block])
|
||
|
||
|
||
def execute_confirmed(store: WorldStore, confirm_id: str, approve: bool, actor: str) -> str:
|
||
"""执行/驳回一条已出卡的 P2 动作(确认卡回传入口;一次性令牌)。
|
||
|
||
权力等级:P2(本函数是 P2 动作的唯一执行通道,§3.3 门禁放行后)。
|
||
Returns: 面向用户的结果文案。
|
||
"""
|
||
pending = harness.take_confirmation(confirm_id) # 取出待确认动作(取后即失效)
|
||
if pending is None: # 过期/重复点击
|
||
return "该确认卡已失效(可能已处理过)。"
|
||
action, params = pending["action"], pending["params"] # 解构动作
|
||
if not approve: # ---- 驳回分支 ----
|
||
write_audit(store.data, store.next_id, actor=actor, category="GATE", action=action + ".reject",
|
||
target=params, power="P2", rationale={"confirmId": confirm_id}, result="DENIED") # 驳回留痕
|
||
store.save() # 落盘
|
||
return "已驳回,未做任何变更。"
|
||
if action == "schedule.publish": # ---- 批准:发布版本 ----
|
||
v = next(x for x in store.data["scheduleVersions"] if x["id"] == params["versionId"]) # 目标版本
|
||
# 回滚防线(§3.3 防线 3):P2 写主干前自动·强制建成对快照(回滚锚点)
|
||
get_checkpoints().create(store.data, label=f"发布前基线 {v['versionNo']}",
|
||
reason="auto:publish", conversation_note=f"批准发布 {v['versionNo']}")
|
||
v["status"] = "PUBLISHED" # 置为已发布
|
||
from datetime import datetime # 局部导入避免顶部循环
|
||
from server.timeutil import fmt_dt # 时间格式化
|
||
v["publishedAt"] = fmt_dt(datetime.now()) # 发布时间
|
||
for po in store.data["productionOrders"]: # 该版本 PO 推进为已确认
|
||
if po["schedulingVersionId"] == v["id"]:
|
||
po["status"] = "CONFIRMED"
|
||
write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="schedule.publish",
|
||
target={"type": "SCHEDULE_VERSION", "id": v["id"]}, power="P2",
|
||
rationale={"confirmId": confirm_id, "approver": actor}) # 发布留痕(谁批的)
|
||
store.save() # 主干写入落盘
|
||
return f"版本 {v['versionNo']} 已发布 ✅ 生产订单已转入执行准备(审计已留痕)。"
|
||
if action == "data.reset": # ---- 批准:重置数据 ----
|
||
# 回滚防线:重置前自动建档(重置本身也可被撤销)
|
||
get_checkpoints().create(store.data, label="重置前基线", reason="auto:reset",
|
||
conversation_note="批准重置数据")
|
||
store.reset() # 重新播种(含落盘)
|
||
write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="data.reset",
|
||
target={"type": "WORLD", "id": 0}, power="P2",
|
||
rationale={"confirmId": confirm_id, "approver": actor}) # 重置留痕
|
||
store.save() # 审计落盘
|
||
return "数据已重置为种子状态 ✅(重置前状态已自动存档,可回滚)"
|
||
if action == "checkpoint.rollback": # ---- 批准:回滚到检查点 ----
|
||
pair = get_checkpoints().get(params["pairId"]) # 取目标快照(完整世界)
|
||
if pair is None: # 快照不存在(被淘汰)
|
||
return "目标检查点不存在(可能已被容量策略淘汰)。"
|
||
# 回滚防线:回滚前先把"现在"也存档(允许撤销这次回滚——时间旅行可往返 §4.4)
|
||
get_checkpoints().create(store.data, label="回滚前状态", reason="auto:rollback",
|
||
conversation_note=f"回滚到 {pair['label']}")
|
||
store.restore(pair["world"]) # 整体替换主干世界(成对恢复的世界侧)
|
||
write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="checkpoint.rollback",
|
||
target={"type": "CHECKPOINT", "id": params["pairId"]}, power="P2",
|
||
rationale={"confirmId": confirm_id, "approver": actor, "label": pair["label"]}) # 回滚留痕
|
||
store.save() # 落盘
|
||
return f"已回滚到检查点【{pair['label']}】({pair['createdAt']})✅ 回滚前状态已自动存档。"
|
||
if action in ("order.upsert", "order.cancel", "order.complete"): # ---- 批准:订单写入 ----
|
||
from server.aps_domain.orders import apply_order_action
|
||
get_checkpoints().create(store.data, label="订单变更前基线", reason=f"auto:{action}",
|
||
conversation_note=f"批准执行 {action}") # P2 写前自动建档
|
||
applied = apply_order_action(store.data, store.next_id, action, params)
|
||
order = applied["order"]
|
||
write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action=action,
|
||
target={"type": "SALES_ORDER", "id": order["id"], "orderNo": order["orderNo"]},
|
||
power="P2", rationale={"confirmId": confirm_id, "approver": actor,
|
||
"beforeStatus": applied.get("beforeStatus"),
|
||
"afterStatus": order["status"]})
|
||
store.save()
|
||
if action == "order.upsert":
|
||
verb = "已新增" if applied.get("created") else "已更新"
|
||
return f"{verb}订单 {order['orderNo']} ✅ 后续试排会使用最新订单池。"
|
||
if action == "order.cancel":
|
||
return f"订单 {order['orderNo']} 已取消 ✅ 后续试排将不再纳入。"
|
||
return f"订单 {order['orderNo']} 已完成 ✅ 后续试排将不再纳入。"
|
||
if action.startswith("master."): # ---- 批准:主数据写入(白名单见 masterdata.MASTER_ACTIONS) ----
|
||
from server.aps_domain.masterdata import apply_master_action
|
||
get_checkpoints().create(store.data, label="主数据变更前基线", reason=f"auto:{action}",
|
||
conversation_note=f"批准执行 {action}") # P2 写前自动建档
|
||
applied = apply_master_action(store.data, store.next_id, action, params)
|
||
write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action=action,
|
||
target={"type": f"MASTER_{applied['kind']}", "id": applied["id"], "name": applied["name"]},
|
||
power="P2", rationale={"confirmId": confirm_id, "approver": actor,
|
||
"before": applied.get("beforeStatus", applied.get("beforeStock")),
|
||
"after": applied.get("afterStatus", applied.get("afterStock"))})
|
||
store.save()
|
||
kind_cn = {"LINE": "产线", "MATERIAL": "物料", "MAINTENANCE": "维保计划",
|
||
"BOM_ITEM": "BOM 明细", "ROUTING_STEP": "工艺步骤"}[applied["kind"]]
|
||
return f"{kind_cn}【{applied['name']}】主数据已更新 ✅ 只影响后续新排产版本(变更前已自动建档)。"
|
||
return "未知动作,已忽略。" # 白名单外兜底(理论不可达)
|
||
|
||
|
||
async def handle_intent(store: WorldStore, session_id: str, intent: IntentResult, actor: str = "planner") -> AgentReply:
|
||
"""意图 → 回复 的总编排(gateway 调用;P0/P1 直通,P2 出卡)。"""
|
||
name = intent.intent # 意图名
|
||
# ---- 排产(P1) ----
|
||
if name == "schedule.run":
|
||
return _run_schedule(store, intent, actor) # 试排并回执
|
||
# ---- 发布(P2 → 确认卡) ----
|
||
if name == "schedule.publish":
|
||
return _stage_publish(store, session_id, actor)
|
||
# ---- 重置数据(P2 → 确认卡) ----
|
||
if name == "data.reset":
|
||
return _stage_reset(store, session_id, actor)
|
||
# ---- 订单状态动作(P2 → 确认卡;对话与页面共用 action) ----
|
||
if name in ("order.cancel", "order.complete"):
|
||
from server.aps_domain.orders import confirmation_for_order_action, find_order
|
||
order_no = str(intent.params.get("orderNo") or "")
|
||
order = find_order(store.data, order_no=order_no)
|
||
if order is None:
|
||
return AgentReply(text=f"没找到订单 {order_no},可以在订单面板核对订单号。")
|
||
title, lines = confirmation_for_order_action(store.data, name, {"id": order["id"]})
|
||
block = harness.stage_confirmation(session_id, name, {"id": order["id"]}, title=title, summary_lines=lines)
|
||
write_audit(store.data, store.next_id, actor=actor, category="GATE", action=name + ".stage",
|
||
target={"type": "SALES_ORDER", "id": order["id"], "orderNo": order["orderNo"]},
|
||
power="P2", rationale={"confirmId": block.props["confirmId"]})
|
||
store.save()
|
||
return AgentReply(text=f"{title} 属于 P2 订单写入,需要你确认。", blocks=[block])
|
||
# ---- 订单分解(P1:MRP 建议草稿,不碰主数据与订单本体) ----
|
||
if name == "order.decompose":
|
||
from server.aps_domain.mrp import decompose_orders, summarize_decomposition
|
||
try:
|
||
result = decompose_orders(store.data, store.next_id, intent.params.get("orderNo"))
|
||
except ValueError as exc:
|
||
return AgentReply(text=str(exc))
|
||
write_audit(store.data, store.next_id, actor=actor, category="ALGO_RUN", action="order.decompose",
|
||
target={"type": "MRP", "id": intent.params.get("orderNo") or "ALL"}, power="P1",
|
||
rationale={"purchase": len(result["purchase"]), "outsource": len(result["outsource"])})
|
||
store.save()
|
||
return AgentReply(text=summarize_decomposition(result))
|
||
# ---- 方案对比(P1:Explore 沙盒,不碰主干 §5.1/§9.7) ----
|
||
if name == "scenario.compare":
|
||
text, block = compare_scenarios(store.data) # 沙盒并行试排三策略
|
||
write_audit(store.data, store.next_id, actor=actor, category="ALGO_RUN", action="scenario.compare",
|
||
target={"type": "SANDBOX", "id": block.blockId}, power="P1",
|
||
rationale={"strategies": [c["strategy"] for c in block.props["cards"]]}) # 沙盒运行留痕
|
||
store.save() # 审计落盘(世界业务数据未变)
|
||
return AgentReply(text=text, blocks=[block]) # 方案卡组交给前端渲染
|
||
# ---- 手动建档(P1:只新增快照 §4.3) ----
|
||
if name == "checkpoint.create":
|
||
label = intent.params.get("label") or f"手动存档 {fmt_date(today0())}" # 缺省自动命名
|
||
meta = get_checkpoints().create(store.data, label=label, reason="manual",
|
||
conversation_note="用户口令建档") # 建成对快照
|
||
write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="checkpoint.create",
|
||
target={"type": "CHECKPOINT", "id": meta["pairId"]}, power="P1",
|
||
rationale={"label": label}) # 建档留痕
|
||
store.save() # 审计落盘
|
||
return AgentReply(text=f"已创建检查点【{label}】(ID: {meta['pairId']})✅\n"
|
||
f"随时可以说“回滚到检查点 {meta['pairId']}”回到此刻。",
|
||
commands=[ViewportCommand(cmd="world.refresh", issuedBy="SYSTEM")]) # 刷新时间线导轨
|
||
# ---- 回滚(P2 → 确认卡 §4.3) ----
|
||
if name == "checkpoint.rollback":
|
||
ckpt = get_checkpoints() # 快照仓
|
||
pair_id = intent.params.get("pairId") # 指定目标(时间线点击合成口令携带)
|
||
pair = ckpt.get(pair_id) if pair_id else ckpt.latest() # 缺省=最近一个
|
||
if pair is None: # 无档可回
|
||
return AgentReply(text="当前没有可回滚的检查点。可先执行“建一个检查点”。")
|
||
block = harness.stage_confirmation( # 回滚是 P2:出确认卡
|
||
session_id, "checkpoint.rollback", {"pairId": pair["pairId"]},
|
||
title=f"回滚到检查点【{pair['label']}】",
|
||
summary_lines=[ # 影响面说明
|
||
f"目标时刻:{pair['createdAt']}(当时版本 {pair.get('versionNo') or '无'})",
|
||
"当前世界状态将被整体替换(对话与世界成对回滚 §4.2)",
|
||
"回滚前会自动再存档一次,本次回滚可撤销",
|
||
])
|
||
write_audit(store.data, store.next_id, actor=actor, category="GATE", action="checkpoint.rollback.stage",
|
||
target={"type": "CHECKPOINT", "id": pair["pairId"]}, power="P2",
|
||
rationale={"confirmId": block.props["confirmId"]}) # 出卡留痕
|
||
store.save() # 审计落盘
|
||
return AgentReply(text=f"回滚是 P2 写操作,需要你确认(目标:{pair['label']})。", blocks=[block])
|
||
# ---- 视口命令族(P0:直接回传命令由前端执行) ----
|
||
if name.startswith("viewport."):
|
||
replies = { # 各命令的回执文案
|
||
"viewport.mode": {"gantt": "已切换为【甘特图】。", "load": "已切换为【负荷热力图】。",
|
||
"due": "已切换为【交期承诺看板】。"}.get(intent.params.get("mode", ""), "已切换视图。"),
|
||
"viewport.filter": "已应用过滤,右侧只显示匹配的工单。",
|
||
"viewport.focus": f"已聚焦产线 {intent.params.get('lineCode', '')},其他产线已隐藏。",
|
||
"viewport.highlight": "已高亮" + ("【超期风险】(红)" if intent.params.get("what") == "overdue" else "【冲突】(黄)") + ",其余淡出。",
|
||
"viewport.timescale": "已切换时间粒度。",
|
||
"viewport.reset": "已重置视口:清除过滤、聚焦与高亮。",
|
||
}
|
||
return AgentReply(text=replies.get(name, "已执行。"), commands=[ # 回传结构化视口命令
|
||
ViewportCommand(cmd=name, params=intent.params, # 命令与槽位原样下发
|
||
target=intent.params.get("lineCode"), issuedBy="LLM")])
|
||
# ---- 知识库检索(P0:只读带出处 §8.2) ----
|
||
if name == "knowledge.query":
|
||
kb = get_knowledge() # 知识库
|
||
title_hint = intent.params.get("assetTitle") # @知识:标题 的直达路径
|
||
if title_hint: # 显式引用优先(§4.7)
|
||
asset = kb.find_by_title(str(title_hint)) # 按标题取
|
||
hits = [{"assetId": asset["assetId"], "title": asset["title"], "kind": asset["kind"],
|
||
"version": asset["version"], "score": 1.0,
|
||
"snippet": asset["content"][:120], "content": asset["content"]}] if asset else []
|
||
else: # 混合检索
|
||
hits = kn_search(kb.assets, str(intent.params.get("query") or ""), top_k=3)
|
||
write_audit(store.data, store.next_id, actor=actor, category="ALGO_RUN", action="knowledge.query",
|
||
target={"type": "KNOWLEDGE", "id": hits[0]["assetId"] if hits else "miss"}, power="P0",
|
||
rationale={"query": intent.params.get("query"), "hits": [h["assetId"] for h in hits]}) # 检索留痕
|
||
store.save() # 审计落盘
|
||
if not hits: # 未命中:诚实告知(禁止编造 §8.1)
|
||
return AgentReply(text="知识库里没有找到相关内容(不编造)。\n"
|
||
"可以换个说法,或说「知识库」查看已有资产清单。")
|
||
# 回答正文:直接引用最相关资产内容 + 出处(强制带出处 §8.1)
|
||
top = hits[0] # 最相关命中
|
||
text = (f"{top['content']}\n\n"
|
||
f"—— 出处:【{top['title']}】{top['version']}({top['kind']})")
|
||
block = UIBlock( # 证据块:全部命中带出处(前端渲染)
|
||
blockId=f"evidence-{top['assetId']}", type="evidence",
|
||
props={"hits": [{k: h[k] for k in ("assetId", "title", "kind", "version", "score", "snippet")}
|
||
for h in hits]})
|
||
return AgentReply(text=text, blocks=[block])
|
||
# ---- 报告生成(P1:产出文档并入知识库 §9.10) ----
|
||
if name == "report.generate":
|
||
report_type = str(intent.params.get("reportType") or "daily") # 报告类型(缺省日报)
|
||
report = build_report(store.data, report_type) # 冻结快照 → 模板生成
|
||
if not report["reportId"]: # 前置条件不满足(无版本/不足两版)
|
||
return AgentReply(text=report["markdown"])
|
||
# 报告入知识库(§9.10 规则3:报告本身成为 RAG 语料)
|
||
asset = get_knowledge().add(kind="report", title=report["title"],
|
||
content=report["markdown"], tags=[report_type, "报告"])
|
||
write_audit(store.data, store.next_id, actor=actor, category="ALGO_RUN", action="report.generate",
|
||
target={"type": "REPORT", "id": report["reportId"]}, power="P1",
|
||
rationale={"reportType": report_type, "assetId": asset["assetId"]}) # 报告留痕
|
||
store.save() # 审计落盘
|
||
block = UIBlock( # 报告块:前端预览 + 下载
|
||
blockId=f"report-{report['reportId']}", type="report",
|
||
props={"reportId": report["reportId"], "title": report["title"],
|
||
"markdown": report["markdown"], "reportType": report_type,
|
||
"assetId": asset["assetId"]})
|
||
return AgentReply(text=f"已生成【{report['title']}】(模板化生成,数字全部取自冻结快照,可复现)。\n"
|
||
f"报告已入知识库(资产 {asset['assetId']}),可下载 Markdown。",
|
||
blocks=[block])
|
||
# ---- KPI 查询(P0) ----
|
||
if name == "query.kpi":
|
||
s = world_summary(store.data) # 取 KPI 摘要
|
||
if not s["hasVersion"]: # 无版本引导
|
||
return AgentReply(text="当前没有排产版本,暂无可计算的版本 KPI。请先生成排产草案。")
|
||
text = (f"当前版本 {s['versionNo']}({s['status']}):\n"
|
||
f"生产订单 {s['poCount']} / 工单 {s['woCount']}\n"
|
||
f"冲突 {s['conflictCount']} 项 · 总延迟 {s['totalTardiness']}h\n"
|
||
f"平均利用率 {round(s['avgUtilization'] * 100)}% · 预估成本 ¥{int(s['totalCost']):,}") # KPI 文案
|
||
return AgentReply(text=text)
|
||
# ---- 帮助(P0) ----
|
||
if name == "help":
|
||
return AgentReply(text=_HELP)
|
||
# ---- 拒识兜底 ----
|
||
return AgentReply(text="这句话我没有把握理解对(置信度不足)。\n" + _HELP)
|