175 lines
12 KiB
Python
175 lines
12 KiB
Python
# ============================================================
|
||
# 意图识别两级管线(moduleId: core-intent, 可重生 ✅)
|
||
# plan.md §9.6:一级规则快路(<10ms,POC 已验证)→ 二级 LLM 语义解析(JSON 强校验)
|
||
# 低置信拒识;LLM 不可用/输出非法 → 自动降级为 unknown(引导用户换说法)
|
||
# ============================================================
|
||
from __future__ import annotations # 前向类型引用
|
||
|
||
import re # 正则快路
|
||
from typing import Any # 类型标注
|
||
|
||
from pydantic import ValidationError # 契约校验异常
|
||
|
||
from server.agent_core.providers import get_provider # LLM Provider
|
||
from server.contracts import IntentResult # 意图结果契约
|
||
|
||
# 世界状态类型别名(快路需要查产线表做聚焦解析)
|
||
World = dict[str, Any]
|
||
|
||
# ---------------- LLM 系统提示词(意图枚举 + 槽位说明 + 输出 Schema) ----------------
|
||
_LLM_SYSTEM = """你是工厂排产系统的意图解析器。把用户的中文口令解析为 JSON(只输出 JSON,不要多余文字)。
|
||
输出格式:{"intent": "...", "params": {...}, "confidence": 0.0~1.0}
|
||
可选 intent 及其 params:
|
||
- schedule.run 试排/排产/重排。params: strategy ∈ [DELIVERY_FIRST交期优先, CAPACITY_BALANCE产能均衡, COST_FIRST成本最优, FIFO先进先出, COMPREHENSIVE综合](默认COMPREHENSIVE), engine ∈ [RULE规则, CP约束规划, GA遗传算法, HYBRID混合](默认HYBRID)
|
||
- schedule.publish 发布/下发当前排产版本。params: {}
|
||
- order.cancel 取消销售订单。params: orderNo=订单号(如 SO20260716001)
|
||
- order.complete 完成销售订单。params: orderNo=订单号(如 SO20260716001)
|
||
- order.decompose 订单分解/展开BOM/生成采购委外建议。params: orderNo=订单号(可选,缺省=全部可排产订单)
|
||
- scenario.compare 多方案/多策略对比试排(沙盒,不影响当前方案)。params: {}
|
||
- checkpoint.create 存档/建检查点/打快照。params: label=快照名(可选)
|
||
- checkpoint.rollback 回滚/回到某个检查点。params: pairId=检查点ID(可选,缺省=最近一个)
|
||
- knowledge.query 查知识库/问规定/问SOP/问策略原理(如"换线有什么规定")。params: query=原始问题
|
||
- report.generate 生成报告。params: reportType ∈ [daily日报, version-diff版本对比]
|
||
- viewport.mode 切换视图。params: mode ∈ [gantt甘特图, load负荷热力, due交期承诺看板]
|
||
- viewport.filter 过滤订单。params: type ∈ [vip, customer]; type=customer 时给 name=客户名
|
||
- viewport.focus 聚焦某条产线。params: lineCode 如 "L001"(产线A=L001 产线B=L002 产线C=L003)
|
||
- viewport.highlight 高亮风险。params: what ∈ [overdue超期, conflict冲突]
|
||
- viewport.timescale 时间粒度。params: scale ∈ [day, hour]
|
||
- viewport.reset 重置视图。params: {}
|
||
- query.kpi 查询指标/KPI/利用率/情况。params: {}
|
||
- data.reset 清空数据重新初始化。params: {}
|
||
- help 询问能做什么。params: {}
|
||
- unknown 无法归类。confidence 给低值。
|
||
只输出一个 JSON 对象。confidence 表示你对解析的把握。"""
|
||
|
||
|
||
# ---------------- 一级:规则快路(移植 POC parseNL,P0 纯解析) ----------------
|
||
def parse_fast(text: str, world: World) -> IntentResult | None:
|
||
"""正则词表快路:命中返回高置信意图;未命中返回 None(交给 LLM)。"""
|
||
t = text.strip() # 去除首尾空白
|
||
|
||
# 快路辅助:构造高置信结果(来源标注 RULE_FAST)
|
||
def hit(intent: str, params: dict | None = None) -> IntentResult:
|
||
return IntentResult(intent=intent, params=params or {}, confidence=0.95, source="RULE_FAST")
|
||
|
||
if re.search(r"重置数据|清空数据|重新初始化", t): # 数据重置
|
||
return hit("data.reset")
|
||
if re.search(r"发布|下发", t): # 发布版本(P2,走门禁)
|
||
return hit("schedule.publish")
|
||
m_order_cancel = re.search(r"(取消|作废).{0,6}(订单)?\s*(SO\d{8,}\d*)", t, re.I)
|
||
if m_order_cancel:
|
||
return hit("order.cancel", {"orderNo": m_order_cancel.group(3).upper()})
|
||
m_order_done = re.search(r"(完成|完结|关闭).{0,6}(订单)?\s*(SO\d{8,}\d*)", t, re.I)
|
||
if m_order_done:
|
||
return hit("order.complete", {"orderNo": m_order_done.group(3).upper()})
|
||
# 订单分解(MRP):分解订单 SOxxx / 订单分解 / 生成采购建议 / 展开 BOM
|
||
m_decomp = re.search(r"(分解|展开).{0,6}(订单)?\s*(SO\d{8,}\d*)", t, re.I)
|
||
if m_decomp:
|
||
return hit("order.decompose", {"orderNo": m_decomp.group(3).upper()})
|
||
if re.search(r"订单分解|分解(全部|所有)?订单|生成(采购|委外)建议|展开\s*BOM|物料需求(计划|分解)|MRP", t, re.I):
|
||
return hit("order.decompose")
|
||
# 方案对比:多方案/多策略/对比几种策略/对比一下(Explore 沙盒 §5.1)
|
||
if re.search(r"多方案|多策略|方案对比|(对比|比较).{0,8}(策略|方案|排法)|对比一下|三种策略|沙盒对比", t):
|
||
return hit("scenario.compare")
|
||
# 报告生成(M3 §9.10):日报 / 版本对比
|
||
if re.search(r"(生成|出|来一份|写)?(排产)?日报", t) and re.search(r"日报", t):
|
||
return hit("report.generate", {"reportType": "daily"})
|
||
if re.search(r"版本对比|对比报告|新旧版本", t) and re.search(r"报告|对比", t):
|
||
return hit("report.generate", {"reportType": "version-diff"})
|
||
# 知识库检索(M3 §8.2):@知识 引用 / 明确问规定与 SOP
|
||
m_kn = re.search(r"@知识[::]\s*(\S{1,24})", t)
|
||
if m_kn:
|
||
return hit("knowledge.query", {"query": m_kn.group(1), "assetTitle": m_kn.group(1)})
|
||
if re.search(r"有什么规定|什么规定|规定是什么|SOP|标准是什么|审批流程|知识库", t, re.I):
|
||
return hit("knowledge.query", {"query": t})
|
||
# 回滚到指定检查点(时间线导轨点击会合成此口令;ID 为 10 位十六进制)
|
||
m_rb = re.search(r"回滚到检查点\s*([0-9a-f]{6,12})", t, re.I)
|
||
if m_rb:
|
||
return hit("checkpoint.rollback", {"pairId": m_rb.group(1)})
|
||
# 回滚到最近检查点(口语形态)
|
||
if re.search(r"回滚|回到上一?个?(检查点|存档|快照)", t):
|
||
return hit("checkpoint.rollback")
|
||
# 手动建档:存档/存个档/检查点/快照(可带名字:"存档 叫 发布前基线")
|
||
m_ck = re.search(r"(存个?档|建一?个?检查点|打个?快照)(?:[,,\s]*(?:叫|为|名字是)?\s*(.{1,16}))?$", t)
|
||
if m_ck and re.search(r"存个?档|检查点|快照", t):
|
||
label = (m_ck.group(2) or "").strip() # 提取可选命名
|
||
return hit("checkpoint.create", {"label": label} if label else {})
|
||
# 执行排产:要求"动词形态"命中(试排/重排/排一版/执行|开始|重新排产),
|
||
# 避免仅出现"排产"名词的闲聊被误判(该类输入应交给二级 LLM 判定)
|
||
if re.search(r"试排|重排|排一版|(执行|开始|重新|跑一?版?)排产", t):
|
||
strategy = ("DELIVERY_FIRST" if re.search(r"交期", t)
|
||
else "CAPACITY_BALANCE" if re.search(r"均衡|产能", t)
|
||
else "COST_FIRST" if re.search(r"成本", t)
|
||
else "FIFO" if re.search(r"FIFO|先进先出", t, re.I)
|
||
else None) # 未点名策略 → 交给偏好个性化(§8.3)
|
||
engine = ("RULE" if re.search(r"规则", t)
|
||
else "CP" if re.search(r"约束|CP", t, re.I)
|
||
else "GA" if re.search(r"遗传|GA", t, re.I)
|
||
else "HYBRID") # 默认混合
|
||
params: dict = {"engine": engine} # 引擎必填
|
||
if strategy: # 用户明确点名才带策略槽位
|
||
params["strategy"] = strategy
|
||
return hit("schedule.run", params)
|
||
if re.search(r"热力|负荷", t): # 负荷热力图
|
||
return hit("viewport.mode", {"mode": "load"})
|
||
if re.search(r"交期|承诺|看板", t): # 交期承诺看板
|
||
return hit("viewport.mode", {"mode": "due"})
|
||
if re.search(r"甘特|排程图", t): # 甘特图
|
||
return hit("viewport.mode", {"mode": "gantt"})
|
||
if re.search(r"只看|过滤", t): # 过滤(VIP / 客户名)
|
||
if re.search(r"VIP", t, re.I): # VIP 过滤
|
||
return hit("viewport.filter", {"type": "vip"})
|
||
m = re.search(r"只看(.{1,8}?)的?(订单|单子)", t) # 提取客户名
|
||
if m:
|
||
return hit("viewport.filter", {"type": "customer", "name": m.group(1)})
|
||
m_code = re.search(r"L00(\d)", t, re.I) # 产线编码匹配
|
||
m_name = re.search(r"产线\s*([ABC])", t) # 产线口语匹配
|
||
if re.search(r"聚焦|放大|定位|只看|看看|看一下", t) and (m_code or m_name): # 聚焦产线
|
||
code = f"L00{m_code.group(1)}" if m_code else None # 编码直取
|
||
if not code and m_name: # 名称转编码(查产线表)
|
||
line = next((l for l in world["lines"] if l["name"].endswith(m_name.group(1))), None)
|
||
code = line["code"] if line else None # 命中则取编码
|
||
if code: # 拿到编码才算命中
|
||
return hit("viewport.focus", {"lineCode": code})
|
||
if re.search(r"超期|逾期|延迟", t): # 高亮超期
|
||
return hit("viewport.highlight", {"what": "overdue"})
|
||
if re.search(r"冲突", t): # 高亮冲突
|
||
return hit("viewport.highlight", {"what": "conflict"})
|
||
if re.search(r"按小时|小时粒度", t): # 小时粒度
|
||
return hit("viewport.timescale", {"scale": "hour"})
|
||
if re.search(r"按天|天粒度", t): # 天粒度
|
||
return hit("viewport.timescale", {"scale": "day"})
|
||
if re.search(r"重置|清除|复位", t): # 重置视口
|
||
return hit("viewport.reset")
|
||
if re.search(r"指标|KPI|利用率|怎么样", t, re.I): # KPI 查询
|
||
return hit("query.kpi")
|
||
if re.search(r"帮助|能做什么|help", t, re.I): # 帮助
|
||
return hit("help")
|
||
return None # 快路未命中 → 交给二级 LLM
|
||
|
||
|
||
# ---------------- 二级:LLM 语义解析(JSON Schema 强校验 + 降级) ----------------
|
||
async def parse_llm(text: str) -> IntentResult:
|
||
"""调用 LLM 做意图分类+槽位填充;输出非法/不可用一律降级为 unknown。"""
|
||
raw = await get_provider().chat_json(_LLM_SYSTEM, text) # 请求 LLM(强制 JSON)
|
||
if raw is None: # LLM 不可用(无密钥/网络/超时)
|
||
return IntentResult(intent="unknown", params={}, confidence=0.0, source="LLM") # 降级拒识
|
||
try:
|
||
raw.setdefault("params", {}) # 容错:缺 params 补空
|
||
raw["source"] = "LLM" # 强制标注来源
|
||
result = IntentResult(**raw) # pydantic 强校验(枚举外意图直接抛错)
|
||
except ValidationError: # 输出不合契约
|
||
return IntentResult(intent="unknown", params={}, confidence=0.0, source="LLM") # 降级拒识
|
||
if result.confidence < 0.5: # 低置信 → 拒识(§9.6 置信度分档)
|
||
return IntentResult(intent="unknown", params=result.params, confidence=result.confidence, source="LLM")
|
||
return result # 合法且够置信 → 采纳
|
||
|
||
|
||
# ---------------- 管线入口 ----------------
|
||
async def recognize(text: str, world: World) -> IntentResult:
|
||
"""两级意图管线总入口:快路优先,未命中走 LLM(P0:纯解析无副作用)。"""
|
||
fast = parse_fast(text, world) # 一级:规则快路
|
||
if fast is not None: # 命中即返回(毫秒级、零成本)
|
||
return fast
|
||
return await parse_llm(text) # 二级:LLM 语义解析(含降级)
|