432 lines
21 KiB
Python
432 lines
21 KiB
Python
|
|
# ============================================================
|
|||
|
|
# 多轮对话状态机(moduleId: core-dialog, 可重生 ✅)
|
|||
|
|
# M-F:听不懂就追问,缺数据就引导。
|
|||
|
|
# ① 澄清槽位机:意图置信 0.5–0.85 / 拒识 → 出「澄清卡」(候选意图按钮),
|
|||
|
|
# 下一句优先按澄清上下文解析(「第一个」「1」或候选关键词直接回填),
|
|||
|
|
# 两轮未解则放弃回兜底。
|
|||
|
|
# ② 引导式排产向导(schedule.wizard):readiness → 缺路线推荐行业模板 /
|
|||
|
|
# 缺工时逐条追问 / 缺日历给默认 → 齐备后自动试排。
|
|||
|
|
# 会话态仅存内存(进程内),不入 world;对世界的写全部走 P2 确认卡。
|
|||
|
|
# ============================================================
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import re
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
from server.contracts import AgentReply, IntentResult, UIBlock
|
|||
|
|
|
|||
|
|
# session_id → {"clarify": {...}|None, "wizard": {...}|None}
|
|||
|
|
_SESSIONS: dict[str, dict[str, Any]] = {}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _state(session_id: str) -> dict[str, Any]:
|
|||
|
|
return _SESSIONS.setdefault(session_id, {"clarify": None, "wizard": None})
|
|||
|
|
|
|||
|
|
|
|||
|
|
def reset_session(session_id: str) -> None:
|
|||
|
|
_SESSIONS.pop(session_id, None)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# 领域路由(意图分层的兜底层):先判「用户在聊哪个域」,
|
|||
|
|
# 域命中而子意图不明 → 澄清卡列出该域常用动作。
|
|||
|
|
# ============================================================
|
|||
|
|
_DOMAINS: list[dict[str, Any]] = [
|
|||
|
|
{"domain": "排产", "pattern": r"排产|排程|试排|排一版|计划|交期|甘特|开始|怎么办",
|
|||
|
|
"candidates": [
|
|||
|
|
("schedule.wizard", "引导式排产向导(从头带我排)"),
|
|||
|
|
("flex.schedule", "柔性排产(能力池直接排一版)"),
|
|||
|
|
("assistant.reply", "先分析现状再决定"),
|
|||
|
|
("flex.capacity", "看瓶颈产能"),
|
|||
|
|
]},
|
|||
|
|
{"domain": "订单", "pattern": r"订单|插单|急单|销售单|下单",
|
|||
|
|
"candidates": [
|
|||
|
|
("order.pool", "查订单池"),
|
|||
|
|
("rush.evaluate", "评估紧急插单"),
|
|||
|
|
("plan.trace", "追溯订单计划"),
|
|||
|
|
]},
|
|||
|
|
{"domain": "主数据", "pattern": r"主数据|物料|BOM|工艺路线|工时|设备|产线|资源|分析.*(文件|数据|项目)|这个文件|文件解析",
|
|||
|
|
"candidates": [
|
|||
|
|
("data.analyze", "分析项目/文件数据与排产缺口"),
|
|||
|
|
("master.query", "查主数据"),
|
|||
|
|
("readiness.query", "数据齐备度 / 工时维护情况"),
|
|||
|
|
("flex.time.update", "维护工时"),
|
|||
|
|
]},
|
|||
|
|
{"domain": "知识", "pattern": r"知识|规定|SOP|文档|工艺模式|导入.*文档",
|
|||
|
|
"candidates": [
|
|||
|
|
("knowledge.query", "查知识库"),
|
|||
|
|
("knowledge.import", "导入知识文档"),
|
|||
|
|
]},
|
|||
|
|
{"domain": "技能", "pattern": r"skill|算法|外部引擎",
|
|||
|
|
"candidates": [
|
|||
|
|
("skill.list", "查看已接入算法 Skill"),
|
|||
|
|
("skill.health", "Skill 健康检查"),
|
|||
|
|
]},
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
_INTENT_LABELS = {c[0]: c[1] for d in _DOMAINS for c in d["candidates"]}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def guess_domain(text: str) -> dict[str, Any] | None:
|
|||
|
|
for d in _DOMAINS:
|
|||
|
|
if re.search(d["pattern"], text, re.I):
|
|||
|
|
return d
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# ① 澄清槽位机
|
|||
|
|
# ============================================================
|
|||
|
|
def needs_clarification(intent: IntentResult) -> bool:
|
|||
|
|
"""低置信(0.4–0.85)需要澄清;通用助理与帮助不澄清。"""
|
|||
|
|
if intent.intent in ("help", "assistant.reply", "data.analyze", "readiness.query"):
|
|||
|
|
return False
|
|||
|
|
if intent.intent == "unknown":
|
|||
|
|
return True
|
|||
|
|
return 0.4 <= intent.confidence < 0.85
|
|||
|
|
|
|||
|
|
|
|||
|
|
def make_clarification(session_id: str, text: str, intent: IntentResult) -> AgentReply | None:
|
|||
|
|
"""构造澄清卡:候选意图(猜测+确认),保存会话澄清态。"""
|
|||
|
|
candidates: list[tuple[str, str]] = []
|
|||
|
|
if intent.intent != "unknown" and intent.intent in _INTENT_LABELS:
|
|||
|
|
candidates.append((intent.intent, _INTENT_LABELS[intent.intent]))
|
|||
|
|
elif intent.intent != "unknown":
|
|||
|
|
candidates.append((intent.intent, intent.intent))
|
|||
|
|
domain = guess_domain(text)
|
|||
|
|
if domain:
|
|||
|
|
for c in domain["candidates"]:
|
|||
|
|
if all(c[0] != x[0] for x in candidates):
|
|||
|
|
candidates.append(c)
|
|||
|
|
candidates = candidates[:3]
|
|||
|
|
st = _state(session_id)
|
|||
|
|
prev = st.get("clarify") or {}
|
|||
|
|
if not candidates and prev.get("candidates"):
|
|||
|
|
# 本句无新线索但澄清上下文还在 → 复用上一轮候选继续追问
|
|||
|
|
candidates = [(c["intent"], c["label"]) for c in prev["candidates"]]
|
|||
|
|
if not candidates:
|
|||
|
|
return None # 无候选 → 交回原兜底(帮助文案)
|
|||
|
|
|
|||
|
|
prev_rounds = prev.get("rounds", 0)
|
|||
|
|
if prev_rounds >= 2: # 两轮未解 → 放弃澄清
|
|||
|
|
st["clarify"] = None
|
|||
|
|
return None
|
|||
|
|
st["clarify"] = {
|
|||
|
|
"originalText": text,
|
|||
|
|
"candidates": [{"intent": c[0], "label": c[1],
|
|||
|
|
"params": intent.params if c[0] == intent.intent else {}}
|
|||
|
|
for c in candidates],
|
|||
|
|
"rounds": prev_rounds + 1,
|
|||
|
|
}
|
|||
|
|
lines = [f"我不太确定你的意思,你是想:(回复序号即可)"]
|
|||
|
|
for i, c in enumerate(candidates, start=1):
|
|||
|
|
lines.append(f"{i}. {c[1]}")
|
|||
|
|
lines.append("都不是的话,请换个说法补充要点。")
|
|||
|
|
block = UIBlock(
|
|||
|
|
blockId=f"clarify-{session_id[:8]}", type="clarify",
|
|||
|
|
props={"question": "我不太确定你的意思,你是想:",
|
|||
|
|
"options": [{"index": i + 1, "intent": c[0], "label": c[1]}
|
|||
|
|
for i, c in enumerate(candidates)],
|
|||
|
|
"originalText": text})
|
|||
|
|
return AgentReply(text="\n".join(lines), blocks=[block], intent=intent)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _resolve_clarify(session_id: str, text: str) -> IntentResult | None:
|
|||
|
|
"""澄清上下文中解析回填:序号 / 候选关键词 → 直接产出意图。"""
|
|||
|
|
st = _state(session_id)
|
|||
|
|
ctx = st.get("clarify")
|
|||
|
|
if not ctx:
|
|||
|
|
return None
|
|||
|
|
t = text.strip()
|
|||
|
|
cands = ctx["candidates"]
|
|||
|
|
picked = None
|
|||
|
|
m = re.match(r"^(?:第?\s*([1-9一二三])\s*个?)$", t)
|
|||
|
|
if m:
|
|||
|
|
num_map = {"一": 1, "二": 2, "三": 3}
|
|||
|
|
idx = num_map.get(m.group(1)) or int(m.group(1))
|
|||
|
|
if 1 <= idx <= len(cands):
|
|||
|
|
picked = cands[idx - 1]
|
|||
|
|
if picked is None:
|
|||
|
|
for c in cands:
|
|||
|
|
if c["label"] and (t in c["label"] or c["label"] in t) and len(t) >= 2:
|
|||
|
|
picked = c
|
|||
|
|
break
|
|||
|
|
if t.lower() == c["intent"].lower():
|
|||
|
|
picked = c
|
|||
|
|
break
|
|||
|
|
if picked:
|
|||
|
|
st["clarify"] = None
|
|||
|
|
return IntentResult(intent=picked["intent"], params=picked.get("params") or {},
|
|||
|
|
confidence=1.0, source="RULE_FAST")
|
|||
|
|
return None # 未回填 → 交回正常识别(rounds 已计)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# ② 引导式排产向导
|
|||
|
|
# ============================================================
|
|||
|
|
_WIZ_CANCEL = r"取消|退出|不排了|算了"
|
|||
|
|
_WIZ_GO = r"^(排|开始|好|可以|确认|继续|继续排产|go)$"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def start_wizard(store, session_id: str, actor: str = "web") -> AgentReply:
|
|||
|
|
"""入口:readiness 检查 → 全绿直接给「试排」确认,否则进入引导流程。"""
|
|||
|
|
st = _state(session_id)
|
|||
|
|
st["clarify"] = None
|
|||
|
|
st["wizard"] = {"step": "start"}
|
|||
|
|
return _wizard_advance(store, session_id, actor)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _wizard_advance(store, session_id: str, actor: str) -> AgentReply:
|
|||
|
|
"""核心推进:重跑 readiness,按缺口类型给下一步。"""
|
|||
|
|
from server.aps_domain.readiness import check_readiness
|
|||
|
|
st = _state(session_id)
|
|||
|
|
wiz = st["wizard"] or {}
|
|||
|
|
world = store.data
|
|||
|
|
report = check_readiness(world)
|
|||
|
|
s = report["summary"]
|
|||
|
|
|
|||
|
|
# 无订单 → 引导录入订单
|
|||
|
|
if s["total"] == 0:
|
|||
|
|
st["wizard"] = None
|
|||
|
|
return AgentReply(text="当前没有待排订单。先录入订单吧——可以说"
|
|||
|
|
"「导入订单」(Excel 批量)或「新建订单 产品X 数量100 交期…」,"
|
|||
|
|
"录完再说「我要排产」。")
|
|||
|
|
|
|||
|
|
# 缺日历 → 给默认日历选项
|
|||
|
|
if any(gi["type"] == "NO_CALENDAR" for gi in report["globalIssues"]):
|
|||
|
|
st["wizard"] = {"step": "fix_calendar"}
|
|||
|
|
return AgentReply(text="缺班次日历(不知道每天几点开工、一周开几天)。\n"
|
|||
|
|
"回复「默认日历」建单班 08:00-17:00(午休1h)周一~周五;"
|
|||
|
|
"或告诉我你们的班制(如「两班 8点到23点 周一到周六」)。",
|
|||
|
|
blocks=[_wizard_block("fix_calendar", "缺班次日历",
|
|||
|
|
["默认日历", "自定义班制"])])
|
|||
|
|
|
|||
|
|
# 缺路线 → 推荐行业模板
|
|||
|
|
no_routing = [r for r in report["orders"]
|
|||
|
|
if any(i["type"] == "NO_ROUTING" for i in r["issues"])]
|
|||
|
|
if no_routing:
|
|||
|
|
target = no_routing[0]
|
|||
|
|
pc = target["productCode"]
|
|||
|
|
pname = next((m.get("name") for m in world.get("flexMaterials") or []
|
|||
|
|
if m.get("code") == pc), pc)
|
|||
|
|
from server.knowledge.routing_templates import recommend_templates
|
|||
|
|
try:
|
|||
|
|
tpls = recommend_templates(f"{pname} {pc}", top_k=3)
|
|||
|
|
except Exception:
|
|||
|
|
tpls = []
|
|||
|
|
if not tpls:
|
|||
|
|
try:
|
|||
|
|
from server.knowledge.routing_templates import list_templates
|
|||
|
|
tpls = list_templates()[:3]
|
|||
|
|
except Exception:
|
|||
|
|
tpls = []
|
|||
|
|
st["wizard"] = {"step": "pick_template", "productCode": pc, "productName": pname,
|
|||
|
|
"templates": [t["code"] for t in tpls]}
|
|||
|
|
lines = [f"订单 {target['orderNo']} 的产品「{pname}」还没有工艺路线。"
|
|||
|
|
f"根据产品特征,推荐这些行业模板:(回复序号采用)"]
|
|||
|
|
for i, t in enumerate(tpls, start=1):
|
|||
|
|
steps_brief = "→".join(x["operationName"] for x in t["steps"][:6])
|
|||
|
|
more = "…" if len(t["steps"]) > 6 else ""
|
|||
|
|
total = sum(x["stdMinDefault"] for x in t["steps"])
|
|||
|
|
lines.append(f"{i}. {t['name']}:{steps_brief}{more}(约 {total:.0f} 分/件)")
|
|||
|
|
lines.append("都不合适可以回复「跳过」,稍后手工维护路线。")
|
|||
|
|
return AgentReply(text="\n".join(lines),
|
|||
|
|
blocks=[_wizard_block("pick_template", f"为 {pname} 选工艺模板",
|
|||
|
|
[f"{i}. {t['name']}" for i, t in enumerate(tpls, 1)] + ["跳过"],
|
|||
|
|
extra={"templates": tpls, "productCode": pc})])
|
|||
|
|
|
|||
|
|
# 缺工时 → 逐工序追问
|
|||
|
|
pending = [(r["productCode"], i["detail"]) for r in report["orders"]
|
|||
|
|
for i in r["issues"] if i["type"] == "TIME_UNMAINTAINED"]
|
|||
|
|
if pending:
|
|||
|
|
# 提取 (product, op) 队列
|
|||
|
|
queue: list[tuple[str, str]] = []
|
|||
|
|
seen = set()
|
|||
|
|
for r in report["orders"]:
|
|||
|
|
for i in r["issues"]:
|
|||
|
|
if i["type"] != "TIME_UNMAINTAINED":
|
|||
|
|
continue
|
|||
|
|
m = re.search(r"工序\s*(\S+?)\s*无工时", i["detail"])
|
|||
|
|
key = (r["productCode"], m.group(1) if m else "")
|
|||
|
|
if key not in seen and key[1]:
|
|||
|
|
seen.add(key)
|
|||
|
|
queue.append(key)
|
|||
|
|
if queue:
|
|||
|
|
pc, op = queue[0]
|
|||
|
|
st["wizard"] = {"step": "fill_time", "queue": queue}
|
|||
|
|
return AgentReply(text=f"还有 {len(queue)} 道工序没有工时。先补第一条:\n"
|
|||
|
|
f"产品 {pc} 的工序「{op}」单件多少分钟?"
|
|||
|
|
f"回复数字(如「45」),或回复「跳过」逐条略过。",
|
|||
|
|
blocks=[_wizard_block("fill_time", f"补工时:{pc} × {op}",
|
|||
|
|
["跳过", "取消"])])
|
|||
|
|
|
|||
|
|
# 无能力设备(模板应用时已自动补能力,这里剩真缺口)
|
|||
|
|
no_eq = [r for r in report["orders"]
|
|||
|
|
if any(i["type"] == "NO_CAPABLE_EQUIPMENT" for i in r["issues"])]
|
|||
|
|
if no_eq:
|
|||
|
|
st["wizard"] = None
|
|||
|
|
details = ";".join(i["detail"] for r in no_eq[:3] for i in r["issues"]
|
|||
|
|
if i["type"] == "NO_CAPABLE_EQUIPMENT")
|
|||
|
|
return AgentReply(text=f"还有工序没有能力设备:{details}。\n"
|
|||
|
|
f"请在主数据页为设备登记工序能力(capabilities),"
|
|||
|
|
f"或说「设备 XX 增加能力 工序YY」;完成后再说「我要排产」。")
|
|||
|
|
|
|||
|
|
# 全绿(或仅剩警告)→ 试排确认
|
|||
|
|
warn_note = ""
|
|||
|
|
if s["withWarnings"]:
|
|||
|
|
warn_note = f"({s['withWarnings']} 张订单带推断工时/缺料告警,结果会显式标注)"
|
|||
|
|
st["wizard"] = {"step": "confirm_run"}
|
|||
|
|
return AgentReply(text=f"数据齐备 ✅ {s['total']} 张订单可排{warn_note}。\n"
|
|||
|
|
f"回复「排」立即按瓶颈锚模式试排;回复「取消」退出向导。",
|
|||
|
|
blocks=[_wizard_block("confirm_run", "数据齐备,可以试排", ["排", "取消"])])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _wizard_block(step: str, title: str, options: list[str],
|
|||
|
|
extra: dict[str, Any] | None = None) -> UIBlock:
|
|||
|
|
return UIBlock(blockId=f"wizard-{step}", type="wizard",
|
|||
|
|
props={"step": step, "title": title, "options": options, **(extra or {})})
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _wizard_turn(store, session_id: str, text: str, actor: str) -> AgentReply | IntentResult | None:
|
|||
|
|
"""向导激活时的每轮处理。返回 AgentReply(继续向导)/ IntentResult(放行执行)/ None(退出向导走正常识别)。"""
|
|||
|
|
from server.agent_core import harness
|
|||
|
|
from server.agent_core.audit import write_audit
|
|||
|
|
st = _state(session_id)
|
|||
|
|
wiz = st.get("wizard") or {}
|
|||
|
|
step = wiz.get("step")
|
|||
|
|
t = text.strip()
|
|||
|
|
|
|||
|
|
if re.search(_WIZ_CANCEL, t):
|
|||
|
|
st["wizard"] = None
|
|||
|
|
return AgentReply(text="已退出排产向导。随时说「我要排产」重新开始。")
|
|||
|
|
|
|||
|
|
if step == "fix_calendar":
|
|||
|
|
if re.search(r"默认日历|默认|单班", t):
|
|||
|
|
store.data["flexCalendar"] = [{
|
|||
|
|
"shiftCode": "D", "startTime": "08:00", "endTime": "17:00",
|
|||
|
|
"breaks": [{"start": "12:00", "end": "13:00"}], "workdays": [1, 2, 3, 4, 5]}]
|
|||
|
|
write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE",
|
|||
|
|
action="flex.resource.patch", target={"type": "CALENDAR", "id": "default"},
|
|||
|
|
power="P1", rationale={"wizard": True, "calendar": "默认单班"})
|
|||
|
|
store.save()
|
|||
|
|
reply = _wizard_advance(store, session_id, actor)
|
|||
|
|
reply.text = "已建默认日历(单班 08:00-17:00,周一~五)✅\n\n" + reply.text
|
|||
|
|
return reply
|
|||
|
|
return AgentReply(text="没听懂班制。回复「默认日历」,或稍后在主数据页维护后再说「继续排产」。")
|
|||
|
|
|
|||
|
|
if step == "pick_template":
|
|||
|
|
if re.search(r"^跳过$", t):
|
|||
|
|
st["wizard"] = {"step": "resume"}
|
|||
|
|
return AgentReply(text="已跳过该产品。稍后可在主数据页维护路线;说「继续排产」推进其它检查。")
|
|||
|
|
codes = wiz.get("templates") or []
|
|||
|
|
idx = None
|
|||
|
|
m = re.match(r"^(?:第?\s*([1-9一二三])\s*个?)", t)
|
|||
|
|
if m:
|
|||
|
|
num_map = {"一": 1, "二": 2, "三": 3}
|
|||
|
|
idx = num_map.get(m.group(1)) or int(m.group(1))
|
|||
|
|
tpl_code = codes[idx - 1] if idx and 1 <= idx <= len(codes) else None
|
|||
|
|
if not tpl_code:
|
|||
|
|
for c in codes:
|
|||
|
|
from server.knowledge.routing_templates import get_template
|
|||
|
|
tpl = get_template(c)
|
|||
|
|
if tpl and tpl["name"] in t:
|
|||
|
|
tpl_code = c
|
|||
|
|
break
|
|||
|
|
if not tpl_code:
|
|||
|
|
return AgentReply(text="请回复模板序号(1/2/3)、「跳过」或「取消」。")
|
|||
|
|
pc = wiz.get("productCode") or ""
|
|||
|
|
pname = wiz.get("productName") or pc
|
|||
|
|
from server.knowledge.routing_templates import get_template
|
|||
|
|
tpl = get_template(tpl_code)
|
|||
|
|
title = f"用模板「{tpl['name']}」生成 {pname} 工艺路线"
|
|||
|
|
block = harness.stage_confirmation(
|
|||
|
|
session_id, "routing.template.apply",
|
|||
|
|
{"templateCode": tpl_code, "productCode": pc, "productName": pname},
|
|||
|
|
title=title,
|
|||
|
|
summary_lines=[f"模板:{tpl['code']}({len(tpl['steps'])} 步)",
|
|||
|
|
"工时取模板区间中值,来源标「模板」,可实测覆盖。",
|
|||
|
|
f"知识出处:{tpl.get('assetId') or '内置'}"])
|
|||
|
|
write_audit(store.data, store.next_id, actor=actor, category="GATE",
|
|||
|
|
action="routing.template.apply.stage",
|
|||
|
|
target={"type": "TEMPLATE", "id": tpl_code}, power="P2",
|
|||
|
|
rationale={"confirmId": block.props["confirmId"], "productCode": pc, "wizard": True})
|
|||
|
|
store.save()
|
|||
|
|
st["wizard"] = {"step": "resume"}
|
|||
|
|
return AgentReply(text=f"{title}——需要你确认(P2)。批准后说「继续排产」推进下一步。",
|
|||
|
|
blocks=[block])
|
|||
|
|
|
|||
|
|
if step == "fill_time":
|
|||
|
|
queue: list = list(wiz.get("queue") or [])
|
|||
|
|
if not queue:
|
|||
|
|
st["wizard"] = {"step": "resume"}
|
|||
|
|
return _wizard_advance(store, session_id, actor)
|
|||
|
|
pc, op = queue[0]
|
|||
|
|
if re.search(r"^跳过$", t):
|
|||
|
|
queue.pop(0)
|
|||
|
|
st["wizard"] = {"step": "fill_time", "queue": queue} if queue else {"step": "resume"}
|
|||
|
|
if queue:
|
|||
|
|
return AgentReply(text=f"已跳过。下一条:产品 {queue[0][0]} 的工序「{queue[0][1]}」单件多少分钟?")
|
|||
|
|
return _wizard_advance(store, session_id, actor)
|
|||
|
|
m = re.search(r"(\d+(?:\.\d+)?)", t)
|
|||
|
|
if not m:
|
|||
|
|
return AgentReply(text=f"请回复分钟数(如「45」)、「跳过」或「取消」。当前:{pc} × {op}")
|
|||
|
|
std_min = float(m.group(1))
|
|||
|
|
title = f"更新工时:{pc} × {op} → {std_min} 分钟/件"
|
|||
|
|
block = harness.stage_confirmation(
|
|||
|
|
session_id, "flex.time.update",
|
|||
|
|
{"productCode": pc, "operationCode": op, "stdMin": std_min, "source": "实测"},
|
|||
|
|
title=title, summary_lines=[f"单件工时 → {std_min} 分钟(来源=实测)"])
|
|||
|
|
write_audit(store.data, store.next_id, actor=actor, category="GATE",
|
|||
|
|
action="flex.time.update.stage",
|
|||
|
|
target={"type": "ROUTING_TIME", "id": f"{pc}/{op}"}, power="P2",
|
|||
|
|
rationale={"confirmId": block.props["confirmId"], "wizard": True})
|
|||
|
|
store.save()
|
|||
|
|
queue.pop(0)
|
|||
|
|
st["wizard"] = {"step": "fill_time", "queue": queue} if queue else {"step": "resume"}
|
|||
|
|
nxt = (f"\n下一条:产品 {queue[0][0]} 的工序「{queue[0][1]}」单件多少分钟?"
|
|||
|
|
if queue else "\n工时都过了一遍。批准确认卡后说「继续排产」。")
|
|||
|
|
return AgentReply(text=f"{title}——已出确认卡(P2)。{nxt}", blocks=[block])
|
|||
|
|
|
|||
|
|
if step == "confirm_run":
|
|||
|
|
if re.match(_WIZ_GO, t) or re.search(r"^试排|排产$", t):
|
|||
|
|
st["wizard"] = None
|
|||
|
|
return IntentResult(intent="flex.schedule", params={"sortMode": "BOTTLENECK"},
|
|||
|
|
confidence=1.0, source="RULE_FAST")
|
|||
|
|
st["wizard"] = None
|
|||
|
|
return None # 用户说了别的 → 退出向导走正常识别
|
|||
|
|
|
|||
|
|
if step == "resume":
|
|||
|
|
if re.search(r"继续|接着|下一步|排产", t):
|
|||
|
|
return _wizard_advance(store, session_id, actor)
|
|||
|
|
st["wizard"] = None
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
# 未知向导态 → 重置
|
|||
|
|
st["wizard"] = None
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# 对话管线钩子(gateway /api/chat 调用)
|
|||
|
|
# ============================================================
|
|||
|
|
def pre_route(store, session_id: str, text: str, actor: str = "web") -> AgentReply | IntentResult | None:
|
|||
|
|
"""识别前钩子:向导 / 澄清上下文优先。"""
|
|||
|
|
st = _state(session_id)
|
|||
|
|
if st.get("wizard"):
|
|||
|
|
out = _wizard_turn(store, session_id, text, actor)
|
|||
|
|
if out is not None:
|
|||
|
|
return out
|
|||
|
|
if st.get("clarify"):
|
|||
|
|
resolved = _resolve_clarify(session_id, text)
|
|||
|
|
if resolved is not None:
|
|||
|
|
return resolved
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def post_route(store, session_id: str, text: str, intent: IntentResult) -> AgentReply | None:
|
|||
|
|
"""识别后钩子:低置信/拒识 → 猜测+确认澄清卡(替代直接吐帮助全文)。"""
|
|||
|
|
if not needs_clarification(intent):
|
|||
|
|
_state(session_id)["clarify"] = None # 高置信 → 清澄清态
|
|||
|
|
return None
|
|||
|
|
return make_clarification(session_id, text, intent)
|