aps-agent/server/agent_core/intent.py

981 lines
61 KiB
Python
Raw 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: 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产能均衡, CHANGEOVER_MIN换型最小化, CAMPAIGN战役合并, COST_FIRST成本最优(换型代理), FIFO先进先出, COMPREHENSIVE综合](默认COMPREHENSIVE), engine ∈ [RULE规则, CP约束规划(OR-Tools), HYBRID混合(RULE热启动+CP改良), GA遗传(暂RULE代跑)];未点名引擎时用排产参数 defaultEngine
- schedule.publish 发布/下发当前排产版本。params: {}
- campaign.preview 战役/批次合并预览。params: windowDays=交期窗口天数(默认7)- order.upsert 新建或修改销售订单。params: orderNo=订单号(仅修改时给,如 SO20260716001), customerName=客户名, customerLevel ∈ [VIP,A,B,C], productCode=成品编码或名称, quantity=数量, deliveryDate=交期(YYYY-MM-DD), priority=1~9, isRush=是否加急
- order.cancel 取消销售订单。params: orderNo=订单号(如 SO20260716001)
- order.complete 完成销售订单。params: orderNo=订单号(如 SO20260716001)
- order.delete 删除销售订单(物理移除)。params: orderNo=订单号
- order.clear 一键清空全部订单与采购委外建议。params: {}
- order.submit 提交订单审核。params: orderNo=订单号
- order.approve 批准订单(可批量)。params: orderNo 或 orderIds
- order.reject 驳回订单。params: orderNo, reviewNote=原因(可选)
- rush.evaluate 紧急插单影响快评(固定轨沙盒)。params: orderNo=已有订单号(可选), customerName, customerLevel, productCode, quantity, deliveryDate, priority, strategy
- rush.apply 采用上次插单快评结果(写主干+草稿排产)。params: {} 或与 evaluate 相同字段
- forecast.query 查看预测/长周期订单。params: {}
- forecast.upsert 新建或编辑预测订单。params: productCode, quantity, bucket∈[DAY,WEEK,MONTH], periodStart, dueDate, confidence, status
- forecast.delete 删除预测订单。params: forecastNo
- forecast.convert 预测转正为销售订单。params: forecastNo
- plan.buckets 时间分桶计划/日周月粗能力。params: mode∈[DAY,WEEK,MONTH,HYBRID], capacityMode∈[FINITE有限,INFINITE无限], horizonDays, includeForecast
- plan.rccp 有限/无限产能粗评估对照。params: mode, horizonDays, includeForecast
- plan.feasibility 交期可行性分析(能否按期/缺口)。params: horizonDays, includeForecast
- plan.inventory 库存投影/库存曲线。params: materialCode, materialType∈[FINISHED_PRODUCT,RAW_MATERIAL], bucket∈[DAY,WEEK], horizonDays, includeForecast
- plan.leveling 产能平衡/削峰建议。params: mode∈[DAY,WEEK,MONTH,HYBRID], targetLoad, horizonDays, includeForecast
- plan.supply 产供方向决策(加班/扩线/外协)。params: mode, targetLoad, horizonDays, includeForecast
- order.decompose 订单分解/展开BOM/生成采购委外建议。params: orderNo=订单号(可选,缺省=全部可排产订单)
- mrp.release 下达/确认下达采购或委外建议单(草稿转正式)。params: orderNo=订单号(可选), kind ∈ [all全部, purchase采购, outsource委外](默认all)
- plan.trace 追溯/钉扎/看订单依据/从哪排的/BOM库存负荷。params: orderNo=订单号(可选,FO-/SO-), track ∈ [fixed, flex, auto]
- master.material.upsert 新建物料主数据。params: code=编码, name=名称, type ∈ [FINISHED_PRODUCT成品, SEMI_FINISHED半成品, RAW_MATERIAL原料], unit=单位, stock=库存, procurementLeadTime=采购前置期天数
- master.changeover.upsert 换型矩阵写入。params: fromFamily, toFamily, setupMinutes, note?; delete=true+id 删除
- changeover.query 查看换型矩阵。params: 无
- master.clear 一键清理主数据(清空)。params: scope ∈ [resource资源, process工艺模型, calendar日历维保, flex柔性资源, all全部]
- master.query 查订单/物料/工序/BOM/工艺路线/主数据概览/柔性资源/委外/采购建议。params: entity ∈ [order, material, operation, bom, routing, overview, flex, mrp], code=订单号或料号(可选), orderRef=last, aspect ∈ [outsource, purchase, all], query=原口令
- params.update 更新排产参数/客户等级权重。params: vipWeight/aWeight/bWeight/cWeight(可选数字), resetDefaults=是否恢复默认
- params.query 查看当前排产参数/客户等级权重。params: {}
- constraint.profile.query 查看约束配置。params: {}
- constraint.profile.save 更新约束配置。params: constraintId, enabled, kind∈[hard,soft], resetDefaults
- data.import 批量导入订单或物料(自然语言多行/CSV)。params: kind ∈ [orders, materials], text=原始导入正文
- flex.site.load 加载现场完整生产路线/替换柔性演示数据。params: routePath=xlsx路径(可选), dataDir=源表目录(可选), includeSiblings=是否带同源其它单
- flex.schedule 柔性排产/能力池排产/虚拟产线/多品种小批量/走全流程排产/滚动排产/把这个订单排产/计划排产/用外部算法试排。params: sortMode ∈ [ASC正排, DESC倒排, BOTTLENECK瓶颈锚](默认BOTTLENECK), window ∈ [realtime实时60m, short短窗2h, mid中窗2d, long长窗7d, full全量](可选), orderNo=订单号(可选), orderRef=last(指代刚查过的订单), engine=EXTERNAL(外部算法), skillId=skill_id(可选), useExternal=true(可选)
- flex.reschedule 短窗重排/日窗重排/局部重排/分级重排。params: level ∈ [L2短窗, L3日窗, L4全局]
- flex.swap L1局部换机/备机接手。params: equipmentCode(如 PRESS-01), woId(可选), markFault(默认true)
- flex.capacity 瓶颈产能/产能池评估/各工序能做多少。params: {}
- flex.simulate_due 询问某产品多少数量何时能交(交期承诺)。params: productCode ∈ [HV-HARNESS高压线束, PDU-UNIT PDU配电单元], quantity=数量
- flex.compare 三模式对比/正排倒排瓶颈锚对比。params: {}
- flex.rush 柔性紧急插单。params: productCode, quantity, dueDate(可选), priority(默认1)
- flex.fault 设备故障/停机/恢复。params: equipmentCode, status ∈ [MAINTENANCE故障, RUNNING恢复]
- flex.resource.patch 改设备区域状态或模具寿命锁定。params: kind ∈ [equipment, mold], code, status/zone/lifeUsed
- conflict.list 查看冲突/冲突中心/有哪些冲突。params: scope ∈ [flex, fixed, all]
- flex.conflict.resolve 解决冲突/修复冲突。params: conflictId, action(可选)
- sap.status SAP连接/集成状态。params: {}
- sap.sync.inbound 从SAP拉单/同步SAP订单/同步库存。params: {}
- sap.sync.outbound 回写SAP/完工回写。params: {}
- mes.status MES连接/车间系统状态。params: {}
- mes.dispatch 下发MES/下发车间。params: track ∈ [flex柔性, fixed固定]
- mes.report 报工/模拟报工。params: woId=工单号(可选), track, progressPct, finish
- scenario.compare 多方案/多策略对比试排(沙盒,不影响当前方案)。params: {}
- scenario.sensitivity 主控参数敏感性分析 Tornado(沙盒)。params: strategy=策略模板(可选)
- sop.compile 把SOP编译成约束规则包预览。params: query=SOP标题或关键词(如换线)
- sop.apply 应用SOP规则包(P2)。params: query=同上 或 pack=编译结果对象
- checkpoint.create 存档/建检查点/打快照。params: label=快照名(可选)
- checkpoint.rollback 回滚/回到某个检查点。params: pairId=检查点ID(可选,缺省=最近一个)
- knowledge.query 查知识库/问规定/问SOP/问策略原理(如"换线有什么规定")。params: query=原始问题
- knowledge.import 导入知识文档(PDF/docx/md)。params: path=本地文件路径, kind ∈ [sop,process,algorithm,case](可选), title=标题(可选)
- schedule.wizard 引导式排产向导(我要排产/帮我排产/怎么排产/不会排产)。params: {}
- readiness.query 数据齐备度检查/工时维护情况(哪些工时没维护、排产数据够不够)。params: {}
- data.analyze 分析项目/文件/当前数据内容,整理盘点并列出排产缺口。params: query=原口令
- folder.analyze 解析工程目录里的 Excel(读内容/样例/能否开排)。params: {}
- folder.schedule 根据工程目录数据导入并试排。params: {}
- assistant.reply 通用排产问答(怎么用/你是谁/解释现状/开放问题)。params: query=原口令
- flex.time.update 更新单件工时(如"把部装工时改成50分钟")。params: operationCode=工序编码或名称, productCode=产品编码(可选), stdMin=分钟数
- skill.list 查看已接入外部算法。params: {}
- skill.health 算法 skill 健康检查。params: skillId(可选)
- skill.register 登记算法 skill。params: skill_id, endpoint, name(可选), track ∈ [flex,fixed], auth(可选)
- skill.enable 启用/停用算法 skill。params: skillId, enabled=true|false
- report.generate 生成报告。params: reportType ∈ [daily日报, version-diff版本对比, plan排产方案], orderNo=订单号(可选), orderRef=last(可选)
- viewport.mode 切换视图。params: mode ∈ [gantt甘特图, load负荷热力, util资源利用率, due交期承诺看板, flex柔性甘特, pool产能池, kpi KPI仪表盘, compare方案对比表]
- 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: {}
- guidance.next 下一步建议/该做什么/空态引导。params: {}
- data.reset 清空数据重新初始化。params: {}
- help 询问能做什么。params: {}
- unknown 无法归类。confidence 给低值。
只输出一个 JSON 对象。confidence 表示你对解析的把握。"""
_LEVEL_MAP = {"VIP": "VIP", "甲": "A", "乙": "B", "丙": "C", "A": "A", "B": "B", "C": "C"}
def _parse_due_date(t: str) -> str | None:
"""从口令里抽交期:支持 YYYY-MM-DD / YYYY/MM/DD / M月D日 / M-D(缺省当年)。"""
import datetime as _dt
m = re.search(r"(20\d{2})[-/.](\d{1,2})[-/.](\d{1,2})", t)
if m:
y, mo, d = int(m.group(1)), int(m.group(2)), int(m.group(3))
else:
m2 = re.search(r"(\d{1,2})\s*月\s*(\d{1,2})\s*[日号]?", t)
if not m2:
return None
y, mo, d = _dt.date.today().year, int(m2.group(1)), int(m2.group(2))
try:
return _dt.date(y, mo, d).strftime("%Y-%m-%d")
except ValueError:
return None
def _order_upsert_slots(t: str, world: World) -> dict:
"""尽力从口令抽取建/改单槽位(缺失项留空,由 handler 追问或用现单兜底)。"""
params: dict[str, Any] = {}
m_no = re.search(r"(SO\d{6,})", t, re.I) # 编辑目标订单号
if m_no:
params["orderNo"] = m_no.group(1).upper()
m_cust = re.search(r"客户\s*[::]?\s*([^\s,,。::]{1,16})", t) \
or re.search(r"(?:给|向|为)\s*([^\s,,。]{1,16}?)\s*(?:新建|新增|下|加|开|录)", t)
if m_cust:
params["customerName"] = m_cust.group(1).strip()
prod = None # 成品解析:先名称/编码扫描
for m in world.get("materials", []):
if m.get("type") == "FINISHED_PRODUCT" and (m["name"] in t or m["code"].lower() in t.lower()):
prod = m
break
if prod:
params["productCode"] = prod["code"]
m_qty = re.search(r"数量\s*[::]?\s*(\d{1,6})", t) \
or re.search(r"(\d{1,6})\s*(?:套|个|件|台|只|pcs)", t, re.I)
if m_qty:
params["quantity"] = int(m_qty.group(1))
due = _parse_due_date(t)
if due:
params["deliveryDate"] = due
m_lvl = re.search(r"\b(VIP)\b|([甲乙丙])类?客户|等级\s*([ABC])", t, re.I)
if m_lvl:
key = (m_lvl.group(1) or m_lvl.group(2) or m_lvl.group(3) or "").upper()
if key in _LEVEL_MAP:
params["customerLevel"] = _LEVEL_MAP[key]
if re.search(r"加急|插单|紧急|急单", t):
params["isRush"] = True
return params
def _work_dir_has_tables() -> bool:
"""任一工程项目目录下已有 Excel/CSV → 开排优先走目录导入。"""
try:
import os
from server.state.projects import get_project_store
snap = get_project_store().snapshot(include_messages=False)
except Exception:
return False
for p in snap.get("projects") or []:
if not isinstance(p, dict):
continue
work = str(p.get("workDir") or "").strip()
if not work or not os.path.isdir(work):
continue
try:
names = os.listdir(work)
except OSError:
continue
if any(
(
re.search(r"\.(xlsx|xlsm|csv|txt|sql)$", n, re.I)
and not n.startswith("~$") and not n.startswith(".")
)
for n in names
):
return True
return False
def _is_imperative_schedule(text: str) -> bool:
"""用户明确要求「现在就排」,不是问概念/向导。"""
t = (text or "").strip()
if re.search(r"什么叫|什么是|啥叫|怎么排|如何排|会不会|能不能排产吗|向导", t):
return False
return bool(re.search(
r"^(给我|帮我|为我|请|麻烦).{0,6}(模拟|试)?排(产|程)?(一下|一版)?[!!。.~~]*$"
r"|^(我要你|你给我|你帮我|要你).{0,6}(模拟|试)?排(产|程)?(一下|一版)?[!!。.~~]*$"
r"|^排产(吧|啊|呀|啦|!|!)?(一下|一版)?$"
r"|^(开始|马上|赶紧|直接).{0,4}(给我|帮我)?(模拟|试)?排(产|程)?(一下|一版)?[!!。.~~]*$"
r"|^(排一下|开排|开干(排产)?|去排产|模拟排产(一下)?|(试|模拟)排(一版|一下)?)[!!。.~~]*$"
r"|我要你排产|给我排产|你给我排产|帮我排产吧|模拟排产一下|帮我模拟排产|给我模拟排产",
t,
))
# ---------------- 一级:规则快路(移植 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")
# 工程图纸:仅命中明确的 DXF/图纸解析、预览或主数据映射表达。
drawing_path = re.search(r"([A-Za-z]:[\\/][^\n\r\"']+?\.dxf)\b", t, re.I)
if re.search(r"(?:DXF|图纸)", t, re.I):
params = {"query": t}
if drawing_path:
params["path"] = drawing_path.group(1)
if re.search(r"预览|可视化|打开|查看", t):
params["drawingAction"] = "preview"
return hit("data.analyze", params)
if re.search(r"映射|转成|生成|写入|入库|物料|BOM|工艺", t, re.I):
params["drawingAction"] = "stage"
return hit("data.analyze", params)
if re.search(r"解析|分析|识别|读取|扫描", t):
params["drawingAction"] = "analyze"
return hit("data.analyze", params)
# 带真实表格/SQL 路径的口令优先按数据处理,不能被句尾「知识库」误吞。
has_data_path = bool(re.search(r"[A-Za-z]:[\\/].*\.(xlsx|xlsm|csv|txt|sql)\b", t, re.I))
if has_data_path and re.search(r"排产|试排|开排", t):
return hit("folder.schedule", {"query": t})
if has_data_path and re.search(r"解析|分析|读取|识别|导入|入库|主数据|知识库", t):
return hit("data.analyze", {"query": t})
# 工程目录:深度解析 / 按目录数据排产(靠前)
if re.search(
r"根据.{0,16}排产|用.{0,8}(目录|文件夹|这些数据).{0,8}排产"
r"|以上数据|这些(数据|文件|表格).{0,12}排|按(上面|这些|目录).{0,8}排"
r"|导入.{0,6}(目录|文件夹).{0,6}(并)?(试)?排",
t,
):
return hit("folder.schedule", {"query": t})
if re.search(
r"文件夹|工程目录|工作目录|"
r"项目(里|里面|内)(的)?(文件|数据|资料)|项目(文件夹|目录)|"
r"解析.{0,8}(文件夹|目录)|"
r"读取.{0,6}(文件夹|目录)|分析.{0,6}(文件夹|目录)|再解析.*目录|重新解析.*目录",
t, re.I,
):
return hit("folder.analyze", {"query": t})
# —— 明确「开排」祈使句:直接干,禁止再甩四选一菜单 ——
# 有工程目录表格 → 按目录导入试排;否则直接柔性排一版
if _is_imperative_schedule(t):
if _work_dir_has_tables():
return hit("folder.schedule", {"query": t})
return hit("flex.schedule", {"sortMode": "BOTTLENECK", "query": t})
# 小白引导(怎么排 / 不会排)才进向导;「我要排产」保留向导兼容
if re.search(
r"怎么排产|排产向导|带我排(一版|产)?|教我排|一步步排|"
r"不(会|懂)排产|第一次排产|从头排产|^我要排产$",
t,
):
return hit("schedule.wizard")
if re.search(r"直接排(一版|产)?|赶紧排|马上排|帮我排一下", t):
return hit("flex.schedule", {"sortMode": "BOTTLENECK"})
# 问候 / 几点了 / 概念 / 能力 → 通用助理(什么都能问)
if re.search(r"^(你好|您好|嗨|hi|hello|在吗)[\s!!.。??]*$", t, re.I) \
or re.search(r"(现在|当前)?(几点|几时了)|几点了|今天(几号|日期|星期几)|现在是几号", t) \
or re.search(r"你是谁|你能做什么|怎么用|如何使用|介绍一下(你自己|系统|助手)?|你会什么", t) \
or re.search(r"(什么叫|什么是|啥叫|解释).{0,8}排产|排产.{0,4}(什么意思|是什么|啥意思)|你会排产|会不会排产", t) \
or re.search(r"讲个笑话|来个笑话|算一下|\d+\s+[+\-*/×÷]\s+\d+", t): # 须空格,勿匹配交期 2026-08
return hit("assistant.reply", {"query": t})
# 闲聊 / 发牢骚(勿用「带我/教我」抢排产口令)
if re.search(
r"天气|下雨|吃饭|午饭|晚饭|累了|无聊|哈哈|呵呵|讲个笑话|"
r"上班好烦|不想干|摸鱼|咋整|怎么办啊",
t, re.I,
) and not re.search(r"排产|排程|试排|排一版", t):
return hit("assistant.reply", {"query": t})
# 开放式项目/现状问答(先于过宽的 KPI「怎么样」)
if re.search(
r"(现在|当前|项目).{0,6}(怎么样|什么情况|进度|状态|有什么数据)"
r"|帮我看看|看看(现在|当前|项目)|解释一下|总结一下|现状"
r"|不知道(该|怎么)|该怎么(办|做|开始)|从哪(里)?开始"
r"|能不能排|随便问问|跟你聊聊",
t,
):
return hit("assistant.reply", {"query": t})
# 分析项目/文件/数据(靠前:避免落到 unknown + 能力清单)
if re.search(
r"【文件解析】|我已提供主数据材料"
r"|分析.{0,16}(文件|数据|项目|excel|表格|内容|附件)"
r"|(这个|该|此).{0,4}文件|文件里面|项目数据|数据内容"
r"|整理.{0,8}(数据|缺口)|排产还缺什么|还缺什么.*排产",
t, re.I,
):
return hit("data.analyze", {"query": t})
# MES 下发须先于泛化「下发」(否则「下发MES」会误进 schedule.publish)
if re.search(r"下发\s*MES|下发车间|MES\s*下发|推送车间", t, re.I):
tr = "fixed" if re.search(r"固定", t) else "flex"
return hit("mes.dispatch", {"track": tr})
if re.search(r"模拟报工|报工回流|工序报工|完工报工", t, re.I):
p: dict = {"track": "fixed" if re.search(r"固定", t) else "flex"}
if re.search(r"全部|一键|批量", t):
p["finishAll"] = True
m_wo = re.search(r"(?:工单|#)\s*(\d+)", t)
if m_wo:
p["woId"] = int(m_wo.group(1))
if re.search(r"完工|100|完成", t):
p["finish"] = True
return hit("mes.report", p)
if re.search(r"MES\s*状态|车间系统状态|执行进度", t, re.I):
return hit("mes.status")
if re.search(r"发布|下发", t): # 发布版本(P2;MES 下发已在上方拦截)
return hit("schedule.publish")
# 批量导入(须先于单条新建:含「导入订单/物料」)
kind = None
from server.aps_domain.intake import detect_import_kind
kind = detect_import_kind(t)
if kind:
return hit("data.import", {"kind": kind, "text": t})
# 删除订单(物理移除;与「取消」区分)
m_order_del = re.search(r"(删除|删掉|移除).{0,6}(订单)?\s*(SO\d{6,})", t, re.I)
if m_order_del:
return hit("order.delete", {"orderNo": m_order_del.group(3).upper()})
# OR-03 订单池审核
if re.search(r"批量批准|全部批准待审|批准全部待审", t):
return hit("order.approve", {"orderIds": "pending"})
m_ap = re.search(r"(批准|通过审核|审核通过)\s*(订单)?\s*(SO\d{6,})", t, re.I)
if m_ap:
return hit("order.approve", {"orderNo": m_ap.group(3).upper()})
m_rj = re.search(r"(驳回|拒绝)\s*(订单)?\s*(SO\d{6,})", t, re.I)
if m_rj:
p = {"orderNo": m_rj.group(3).upper()}
m_note = re.search(r"原因[::\s]+(.+)$", t)
if m_note:
p["reviewNote"] = m_note.group(1).strip()
return hit("order.reject", p)
m_sub = re.search(r"(提交审核|送审|提交)\s*(订单)?\s*(SO\d{6,})", t, re.I)
if m_sub:
return hit("order.submit", {"orderNo": m_sub.group(3).upper()})
if re.search(r"提交审核|送审", t) and re.search(r"订单", t):
return hit("order.submit", {})
if re.search(r"订单池|待审核订单|订单看板", t):
return hit("order.pool")
# 一键清理订单池 / 主数据板块
if re.search(r"(一键)?(清理|清空|清除).{0,6}(全部)?(销售)?订单|清空订单池", t):
return hit("order.clear")
if re.search(r"(一键)?(清理|清空|清除).{0,8}(主数据|工艺模型|物料|维保|产线资源)", t):
scope = ("process" if re.search(r"工艺|物料|BOM|工序|路线", t)
else "calendar" if re.search(r"维保|日历", t)
else "resource" if re.search(r"资源|产线", t)
else "all")
return hit("master.clear", {"scope": scope})
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()})
# 委外/采购建议问答(须先于下达/分解动作与泛化查数)
if re.search(r"委外|外协|采购建议|分解建议", t) and not re.search(
r"下达|确认下达|正式下单|转正式|生成(采购|委外)建议|订单分解|分解(全部|所有)?订单|展开\s*BOM", t, re.I
):
from server.aps_domain.master_query import extract_code, _mrp_aspect
code = extract_code(t)
params: dict = {"entity": "mrp", "query": t, "aspect": _mrp_aspect(t)}
if code:
params["code"] = code
elif re.search(r"(这个|该|此|本).{0,4}订单|订单.{0,6}(有没有|有无|有|吗)", t):
params["orderRef"] = "last"
return hit("master.query", params)
# 订单分解(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"(下达|确认下达|正式下单|转正式).{0,6}(采购|委外|建议单?|MRP)|把.{0,8}建议.{0,4}下达|建议单下达", t, re.I):
kind_mrp = ("purchase" if re.search(r"采购", t) and not re.search(r"委外", t)
else "outsource" if re.search(r"委外", t) and not re.search(r"采购", t)
else "all")
m_no = re.search(r"(SO\d{6,})", t, re.I)
p: dict = {"kind": kind_mrp}
if m_no:
p["orderNo"] = m_no.group(1).upper()
return hit("mrp.release", p)
# 计划追溯 / 订单钉扎(OR-06):固定 SO 或柔性 FO
if re.search(r"追溯|钉扎|排产依据|怎么排的|从哪排|单据链|看(一下)?(订单)?依据|(BOM|库存|负荷).{0,6}(追溯|依据)|订单.{0,4}(追溯|依据|钉扎)", t, re.I):
m_no = re.search(r"(SO\d{6,}|FO-[\w-]+|RUSH-\d+)", t, re.I)
params: dict = {}
if m_no:
params["orderNo"] = m_no.group(1).upper()
if re.search(r"柔性|钉扎|FO-|虚拟产线", t, re.I) or (params.get("orderNo") or "").startswith(("FO-", "RUSH-")):
params["track"] = "flex"
elif re.search(r"固定|销售订单", t):
params["track"] = "fixed"
return hit("plan.trace", params)
if re.search(r"订单分解|分解(全部|所有)?订单|生成(采购|委外)建议|展开\s*BOM|物料需求(计划|分解)|MRP", t, re.I):
return hit("order.decompose")
# 新建物料(主数据对话录入)
if re.search(r"(新建|新增|创建|录入).{0,4}物料|加一?个?(原料|成品|半成品)", t):
from server.aps_domain.intake import parse_material_slots
return hit("master.material.upsert", parse_material_slots(t))
# MD-06 换型矩阵
if re.search(r"查看换型|换型矩阵|产品族换型|打开换型", t):
return hit("changeover.query")
m_co = re.search(
r"(设置|录入|修改|更新)?换型\s*([A-Za-z0-9_-]+)\s*(到|→|->|至)\s*([A-Za-z0-9_-]+)\s*(\d+(?:\.\d+)?)\s*分?",
t, re.I,
)
if m_co:
return hit("master.changeover.upsert", {
"fromFamily": m_co.group(2).upper(),
"toFamily": m_co.group(4).upper(),
"setupMinutes": float(m_co.group(5)),
})
# 主数据/订单只读检索(实例数据;工艺模式/模板走知识库)
if not re.search(r"模式|模板|怎么生成|总则|车削|铣削|磨削|特征驱动|知识清单", t) and (
re.search(r"(查|查看|看看|检索|有哪些|列出|查询).{0,8}(订单|物料|成品|原料|工序|BOM|物料清单|工艺路线|主数据|柔性资源|能力池)|主数据概览|订单池里有什么|现在有哪些订单", t, re.I)
or re.search(r"查(一下)?\s*(订单|物料|BOM|工艺路线|工序)\s*[\w\-]{4,}", t)
):
from server.aps_domain.master_query import detect_entity, extract_code
ent = detect_entity(t)
code = extract_code(t)
params: dict = {"entity": ent, "query": t}
if code:
params["code"] = code
return hit("master.query", params)
# M-F 引导式排产向导(靠前入口已覆盖;此处兜底小白说法)
if re.search(r"^我要排产$|怎么排产|排产向导|不(会|懂)排产|第一次排产|从头排产", t):
return hit("schedule.wizard")
# 「帮我排产」等祈使句已在上方走 flex/folder;此处勿再抢
# 现场完整生产路线导入(须先于泛化「柔性排产」)
if re.search(r"加载现场(生产)?(订单)?路线|导入完整生产路线|加载现场订单|替换柔性演示|现场数据导入", t):
p: dict = {}
if re.search(r"仅主单|只要主单|不带其它", t):
p["includeSiblings"] = False
return hit("flex.site.load", p)
# 新建/编辑订单(order.upsert,P2):动词 + "订单";尽力抽取槽位,缺失由 handler 追问
if re.search(r"(新建|新增|创建|录入|下|加|新来|来).{0,4}(一?[张个笔条])?\s*(销售)?订单|录单|建单|开一?张单", t) \
or re.search(r"(修改|编辑|更新|改).{0,4}(销售)?订单\s*(SO\d{6,})", t, re.I):
return hit("order.upsert", _order_upsert_slots(t, world))
# IND-02 SOP→约束(须在知识库问答之前,避免「SOP」只走 knowledge.query)
if re.search(r"应用.{0,8}(SOP|规则包)|把.{0,12}(SOP|规则).{0,8}(应用|落地|写入约束)", t, re.I):
q = "换线"
if re.search(r"齐套|库存|缺料", t):
q = "齐套"
elif re.search(r"插单|急单", t):
q = "插单"
elif re.search(r"换线|换型", t):
q = "换线"
m_t = re.search(r"[《「]?([\u4e00-\u9fffA-Za-z0-9\-_]{2,24}SOP)[》」]?", t)
if m_t:
q = m_t.group(1)
return hit("sop.apply", {"query": q})
if re.search(r"编译.{0,6}(SOP|规则)|SOP.{0,6}(编译|转约束|规则包)|把.{0,8}SOP.{0,8}(编译|转成约束)", t, re.I):
q = "换线"
if re.search(r"齐套|库存|缺料", t):
q = "齐套"
elif re.search(r"插单|急单", t):
q = "插单"
elif re.search(r"换线|换型", t):
q = "换线"
return hit("sop.compile", {"query": q})
# SC-06 敏感性分析(须在方案对比之前,避免「对比参数」误触)
if re.search(r"敏感性|敏感度|Tornado|龙卷风图|参数扰动|主控参数分析", t, re.I):
params: dict[str, Any] = {}
if re.search(r"交期优先", t):
params["strategy"] = "DELIVERY_FIRST"
elif re.search(r"均衡", t):
params["strategy"] = "CAPACITY_BALANCE"
elif re.search(r"换型", t):
params["strategy"] = "CHANGEOVER_MIN"
elif re.search(r"战役|批次", t):
params["strategy"] = "CAMPAIGN"
return hit("scenario.sensitivity", params)
# 方案对比:多方案/多策略/对比几种策略/对比一下(Explore 沙盒 §5.1)
if re.search(r"多方案|多策略|方案对比|(对比|比较).{0,8}(策略|方案|排法)|对比一下|三种策略|沙盒对比", t):
return hit("scenario.compare")
# 报告生成(M3 §9.10):日报 / 版本对比 / 排产方案
if re.search(r"排产方案报告|排产报告|方案报告|下载排产|导出排产|生成排产方案|下载方案报告", t):
from server.aps_domain.master_query import extract_code
params: dict = {"reportType": "plan"}
code = extract_code(t)
if code:
params["orderNo"] = code
elif re.search(r"(这个|该|此|本).{0,4}订单|本单|当前订单", t):
params["orderRef"] = "last"
return hit("report.generate", params)
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"})
# RAG:导入知识文档(须在知识问答之前,避免「知识」抢答)
m_imp = re.search(
r"(?:导入|加载|入库).{0,6}知识(文档|文件|资料)?\s*(.+)?$|知识(文档|文件).{0,6}(导入|入库)\s*(.+)?$",
t, re.I)
if m_imp or re.search(r"^导入知识", t):
path = None
m_path = re.search(r"([A-Za-z]:[\\/][^\s\"']+\.\w{1,8}|/[^\s\"']+\.\w{1,8})", t)
if m_path:
path = m_path.group(1).strip().rstrip("。,,")
else:
# 取「导入知识文档」后的路径片段
m2 = re.search(r"导入知识(?:文档|文件|资料)?\s+(.+)$", t)
if m2:
path = m2.group(1).strip().rstrip("。,,")
kind = "sop"
if re.search(r"工艺", t):
kind = "process"
elif re.search(r"算法", t):
kind = "algorithm"
elif re.search(r"案例", t):
kind = "case"
return hit("knowledge.import", {"path": path, "kind": kind, "query": t})
# 外部算法 Skill(须在知识问答 / 柔性排产之前)
if re.search(r"(查看|列出|有哪些).{0,6}(算法\s*)?(skill|技能)|已接入算法|算法\s*skill\s*清单|查看已接入算法", t, re.I):
return hit("skill.list")
if re.search(r"(skill|算法).{0,6}健康检查|健康检查.{0,6}(skill|算法)|检查算法(服务|skill)", t, re.I):
# 仅接受 ASCII skill id(algo.xxx 或 skillId=xxx),避免「健康检查」中文被 \w 吞掉
m_sid = re.search(r"(?:skill[_ ]?id|skill)\s*[=::\s]+(algo\.[A-Za-z0-9._-]+|[A-Za-z][A-Za-z0-9._-]*)", t, re.I)
if not m_sid:
m_sid = re.search(r"(algo\.[A-Za-z0-9._-]+)", t, re.I)
sid = m_sid.group(1) if m_sid else None
return hit("skill.health", {"skillId": sid} if sid else {})
if re.search(r"登记(算法)?\s*skill|注册(算法)?\s*skill|接入(算法|外部算法)", t, re.I):
params: dict[str, Any] = {}
m_id = re.search(r"(?:id|skill[_]?id)\s*[=::]\s*([\w.-]+)", t, re.I)
m_ep = re.search(r"(?:endpoint|url|端点)\s*[=::]\s*(\S+)", t, re.I)
m_name = re.search(r"(?:name|名称)\s*[=::]\s*(\S+)", t, re.I)
if m_id:
params["skill_id"] = m_id.group(1)
if m_ep:
params["endpoint"] = m_ep.group(1).rstrip(",,。")
if m_name:
params["name"] = m_name.group(1)
if re.search(r"固定轨|fixed", t, re.I):
params["track"] = "fixed"
else:
params["track"] = "flex"
return hit("skill.register", params)
if re.search(r"(启用|开启|打开|停用|禁用|关闭)\s*(算法\s*)?skill", t, re.I):
enabled = not bool(re.search(r"停用|禁用|关闭", t))
m_sid = re.search(r"skill\s+(algo\.[A-Za-z0-9._-]+|[A-Za-z][A-Za-z0-9._-]*)", t, re.I)
sid = m_sid.group(1) if m_sid else None
return hit("skill.enable", {"skillId": sid, "enabled": enabled, "query": t})
if re.search(r"用外部算法(试排|排产)|外部算法试排|切换算法\s*skill|走外部(算法|skill)排产", t, re.I):
params: dict[str, Any] = {"sortMode": "BOTTLENECK", "engine": "EXTERNAL", "useExternal": True}
m_sid = re.search(r"(?:skill|算法)\s+(algo\.[A-Za-z0-9._-]+)", t, re.I)
if not m_sid:
m_sid = re.search(r"(algo\.[A-Za-z0-9._-]+)", t, re.I)
if m_sid:
params["skillId"] = m_sid.group(1)
return hit("flex.schedule", params)
# M-B 数据齐备度 / 工时维护(须在知识问答之前,避免「工时」被吞)
if re.search(r"工时(维护)?情况|哪些工时(还)?(没|未|待)维护|工时(缺失|齐不齐|完整吗)"
r"|数据(齐备|完整)度|排产数据(检查|齐不齐|够不够)|readiness", t, re.I):
return hit("readiness.query")
m_time = re.search(r"把\s*(\S{1,24}?)\s*(?:的)?工时(?:改|设|调)(?:成|为|到)?\s*(\d+(?:\.\d+)?)\s*分钟?", t)
if m_time:
target = m_time.group(1).strip()
params_t: dict[str, Any] = {"stdMin": float(m_time.group(2)), "operationCode": target}
# 「把产品X的部装工时…」形态:产品 空格/的 工序
m_pc = re.search(r"把\s*([A-Za-z0-9\-]{4,})\s+(\S{1,16}?)\s*(?:的)?工时", t)
if m_pc:
params_t = {"stdMin": float(m_time.group(2)),
"productCode": m_pc.group(1), "operationCode": m_pc.group(2)}
return hit("flex.time.update", params_t)
# 知识库检索(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|标准是什么|审批流程|知识库"
r"|机加工|工艺路线生成|工艺路线模式|工艺模式|车削工艺|铣削|磨削|装配工艺"
r"|特征驱动|工艺怎么(生成|排)|知识清单|有哪些知识|城轨机构装配路线", 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 {})
if re.search(r"MES\s*状态|车间系统状态|执行进度", t, re.I):
return hit("mes.status")
# SAP 集成(MD-05)
if re.search(r"回写\s*SAP|完工回写|推送\s*SAP|出站同步", t, re.I):
return hit("sap.sync.outbound")
if re.search(r"从\s*SAP\s*拉|同步\s*SAP|拉\s*SAP|入站同步|同步库存|SAP\s*拉单", t, re.I):
return hit("sap.sync.inbound")
if re.search(r"SAP\s*状态|SAP\s*连接|集成状态", t, re.I):
return hit("sap.status")
# 冲突中心 / 修复(EX-03)
if re.search(r"解决冲突|修复冲突|一键修复|处理冲突", t):
m_id = re.search(r"#?\s*(\d+)", t)
return hit("flex.conflict.resolve",
{"conflictId": int(m_id.group(1))} if m_id else {})
if re.search(r"查看冲突|冲突中心|有哪些冲突|冲突列表|看冲突", t):
scope = ("all" if re.search(r"全部|所有", t)
else "fixed" if re.search(r"固定", t) else "flex")
return hit("conflict.list", {"scope": scope})
# L1 局部换机须先于故障口令(同含设备码)
if re.search(r"局部换机|L1\s*换机|备机接手|换到备机|换机\s*(PRESS|WELD|CUT|TEST|LEAK)", t, re.I):
code = "PRESS-01"
m_code = re.search(r"(PRESS|WELD|CUT|TEST|LEAK)-\d+", t, re.I)
if m_code:
code = m_code.group(0).upper()
mark = not bool(re.search(r"不标故障|仅换机", t))
return hit("flex.swap", {"equipmentCode": code, "markFault": mark})
# 分级重排须先于普通柔性排产
if re.search(r"短窗重排|日窗重排|局部重排|分级重排|窗内重排|L2重排|L3重排|L4重排", t, re.I):
level = ("L4" if re.search(r"L4|全局", t, re.I)
else "L3" if re.search(r"L3|日窗|中窗|2\s*天", t, re.I)
else "L2")
return hit("flex.reschedule", {"level": level})
# 三模式对比须先于「正排/倒排」单模式排产
if re.search(r"三模式|正倒排对比|模式对比|(对比|比较).{0,6}(正排|倒排|瓶颈)", t):
return hit("flex.compare")
# 设备故障 / 恢复(DY-01)
if re.search(r"设备故障|恢复设备|设备.{0,8}(故障|坏了|宕机|停机|恢复)|(PRESS|WELD)-\d+.{0,6}(故障|停机|恢复)", t, re.I):
status = "RUNNING" if re.search(r"恢复|修好|上线", t) else "MAINTENANCE"
code = "WELD-01" if re.search(r"焊接|激光|WELD", t, re.I) else "PRESS-01"
m_code = re.search(r"(PRESS|WELD|CUT|TEST|LEAK)-\d+", t, re.I)
if m_code:
code = m_code.group(0).upper()
return hit("flex.fault", {"equipmentCode": code, "status": status})
# 柔性紧急插单(DY-01):须带「柔性」以免与 OR-04 固定轨插单冲突
if re.search(r"柔性\s*(紧急)?插单|柔性急单", t):
code = "PDU-UNIT" if re.search(r"PDU", t, re.I) else "HV-HARNESS"
qty_m = re.search(r"(\d+)\s*(套|件|个)", t)
qty = int(qty_m.group(1)) if qty_m else 50
return hit("flex.rush", {"productCode": code, "quantity": qty, "priority": 1})
# OR-04 固定轨紧急插单:采用 / 快评
if re.search(r"采用插单|确认采用插单|应用插单|插单采用", t):
return hit("rush.apply")
if re.search(r"插单快评|评估插单|插单影响|紧急插单评估|插单评估", t) or (
re.search(r"紧急插单|加急插单|插一?[张个]?急单", t) and not re.search(r"柔性", t)
):
params: dict[str, Any] = {}
m_so = re.search(r"(SO\d{8,})", t, re.I)
if m_so:
params["orderNo"] = m_so.group(1).upper()
qty_m = re.search(r"(\d+)\s*(套|件|个)", t)
if qty_m:
params["quantity"] = int(qty_m.group(1))
if not params.get("orderNo"):
if re.search(r"控制器A|A型|CTRL-A", t, re.I):
params["productCode"] = "CTRL-A"
elif re.search(r"控制器B|B型|CTRL-B", t, re.I):
params["productCode"] = "CTRL-B"
elif re.search(r"控制器C|C型|CTRL-C", t, re.I):
params["productCode"] = "CTRL-C"
else:
params.setdefault("productCode", "CTRL-A")
if re.search(r"交期优先", t):
params["strategy"] = "DELIVERY_FIRST"
elif re.search(r"产能均衡|均衡", t):
params["strategy"] = "CAPACITY_BALANCE"
m_cust = re.search(r"(?:给|为|客户)\s*([^\s,,。]{2,12})", t)
if m_cust and m_cust.group(1) not in ("插单", "急单", "订单"):
params["customerName"] = m_cust.group(1)
return hit("rush.evaluate", params)
# 把这个订单排产 / 给订单xxx做计划排产(须先于泛化「柔性排产」「试排」)
if re.search(r"排产", t) and not re.search(r"查|查看|有哪些|插单|分解|追溯|发布", t) and (
re.search(r"(这个|该|此|本).{0,4}订单|(给|把|对).{0,8}订单|订单\s*[\dA-Za-z\-]{5,}.{0,12}排产|计划排产", t)
):
from server.aps_domain.master_query import extract_code
code = extract_code(t)
# 现场/柔性订单优先走能力池排产
has_flex = bool(world.get("flexOrders"))
params: dict = {"sortMode": "BOTTLENECK"}
if code:
params["orderNo"] = code
else:
params["orderRef"] = "last"
if has_flex or re.search(r"柔性|能力池|虚拟产线", t):
return hit("flex.schedule", params)
# 仅固定轨时走规则试排
return hit("schedule.run", params)
# 柔性排产(能力池 + 虚拟产线):须在固定产线 schedule.run 之前判定
if re.search(r"柔性排产|柔性排一?版|能力池排产|按能力池|虚拟产线|多品种小批量"
r"|全流程排产|走一?个?全流程|正排|顺排|倒排|逆排|瓶颈锚|滚动排产|短窗排产|中窗排产"
r"|实时排产|分钟级排产", t):
mode = ("ASC" if re.search(r"正排|顺排", t)
else "DESC" if re.search(r"倒排|逆排", t)
else "BOTTLENECK")
win = ("realtime" if re.search(r"实时|分钟级", t)
else "short" if re.search(r"短窗", t)
else "mid" if re.search(r"中窗|日窗|2\s*天", t)
else "long" if re.search(r"长窗|周窗|7\s*天", t)
else "full" if re.search(r"全量|全窗", t)
else None)
params: dict = {"sortMode": mode}
if win:
params["window"] = win
elif re.search(r"滚动", t):
params["window"] = "short"
return hit("flex.schedule", params)
# 瓶颈产能评估(瓶颈产能法)
if re.search(r"瓶颈产能|产能池|产能评估|各?工序产能|瓶颈在哪|产能瓶颈|能做多少|日产能", t):
return hit("flex.capacity")
# 交期承诺模拟:问「何时/多久能交/做完」+ 产品 + 数量
m_prod = re.search(r"(高压线束|线束|PDU|HV-?HARNESS|PDU-?UNIT)", t, re.I)
m_qty = re.search(r"(\d{1,6})\s*(套|个|件|pcs)?", t, re.I)
if (re.search(r"(什么时候|多久|何时|几天).{0,6}(能|可以)?.{0,2}(交|交货|完成|做完|下线|出货)|交期(承诺|模拟|评估)|能不能按期",
t) and m_prod and m_qty):
code = "PDU-UNIT" if re.search(r"PDU", t, re.I) else "HV-HARNESS"
return hit("flex.simulate_due", {"productCode": code, "quantity": int(m_qty.group(1))})
# OR-05 预测订单(须在 schedule.run 之前)
if re.search(r"预测转正|转正预测", t):
m_fc = re.search(r"(FC\d{8,})", t, re.I)
return hit("forecast.convert", {"forecastNo": m_fc.group(1).upper()} if m_fc else {})
if re.search(r"删除预测|去掉预测", t):
m_fc = re.search(r"(FC\d{8,})", t, re.I)
return hit("forecast.delete", {"forecastNo": m_fc.group(1).upper()} if m_fc else {})
if re.search(r"新建预测|创建预测|加一?[条个]?预测", t):
params: dict[str, Any] = {"status": "ACTIVE", "bucket": "WEEK"}
qty_m = re.search(r"(\d+)\s*(套|件|个)", t)
if qty_m:
params["quantity"] = int(qty_m.group(1))
if re.search(r"控制器A|CTRL-A", t, re.I):
params["productCode"] = "CTRL-A"
elif re.search(r"控制器B|CTRL-B", t, re.I):
params["productCode"] = "CTRL-B"
elif re.search(r"控制器C|CTRL-C", t, re.I):
params["productCode"] = "CTRL-C"
else:
params["productCode"] = "CTRL-A"
if re.search(r"月", t):
params["bucket"] = "MONTH"
elif re.search(r"日|天", t):
params["bucket"] = "DAY"
return hit("forecast.upsert", params)
if re.search(r"预测订单|查看预测|长周期(订单|需求)|预测需求|预测台账", t):
return hit("forecast.query")
# PL-06 产供方向(先于削峰通用口令,避免「外协建议」落到 MRP)
if re.search(r"产供(方向|决策)?|产能决策|加班建议|扩线建议|外协建议|外协产能|打开产供", t):
params: dict[str, Any] = {"mode": "WEEK", "includeForecast": True, "targetLoad": 0.85}
if re.search(r"混合", t):
params["mode"] = "HYBRID"
elif re.search(r"月", t):
params["mode"] = "MONTH"
elif re.search(r"日|天", t):
params["mode"] = "DAY"
if re.search(r"不含预测|不要预测|仅确定", t):
params["includeForecast"] = False
return hit("plan.supply", params)
# PL-05 产能平衡/削峰(须在「产能均衡试排」之前;勿抢 schedule.run)
if re.search(r"削峰|产能平衡|负荷削峰|计划层均衡|平衡负荷|打开削峰|leveling", t, re.I):
params: dict[str, Any] = {"mode": "WEEK", "includeForecast": True, "targetLoad": 0.85}
if re.search(r"混合", t):
params["mode"] = "HYBRID"
elif re.search(r"月", t):
params["mode"] = "MONTH"
elif re.search(r"日|天", t):
params["mode"] = "DAY"
elif re.search(r"周", t):
params["mode"] = "WEEK"
if re.search(r"不含预测|不要预测|仅确定", t):
params["includeForecast"] = False
return hit("plan.leveling", params)
# PL-04 库存投影
if re.search(r"库存投影|库存曲线|投影库存|物料库存投影|打开库存投影", t):
params: dict[str, Any] = {"includeForecast": True, "bucket": "DAY", "horizonDays": 30}
if re.search(r"按周|周投影|周桶", t):
params["bucket"] = "WEEK"
if re.search(r"原料|原材料", t):
params["materialType"] = "RAW_MATERIAL"
elif re.search(r"成品", t):
params["materialType"] = "FINISHED_PRODUCT"
m_code = re.search(r"(CTRL-[ABC]|PCB-00[12]|RC-001|CASE-001|SCR-001|WIRE-001|LABEL-001)", t, re.I)
if m_code:
params["materialCode"] = m_code.group(1).upper()
if re.search(r"不含预测|不要预测|仅确定", t):
params["includeForecast"] = False
return hit("plan.inventory", params)
# PL-03 可行性(先于分桶/粗能力通用口令)
if re.search(r"可行性(分析|评估|检查)?|能否按期|交期可行性|计划可行性|可行不可行", t):
params: dict[str, Any] = {"includeForecast": True}
if re.search(r"不含预测|不要预测|仅确定|只要订单", t):
params["includeForecast"] = False
return hit("plan.feasibility", params)
# PL-02 粗能力双模式(先于通用分桶口令)
if re.search(r"有限无限|无限有限|粗能力对比|RCCP对比|对比粗能力", t, re.I):
return hit("plan.rccp", {"mode": "HYBRID", "includeForecast": True})
if re.search(r"无限产能|无限粗(能力|评|估)|无限RCCP", t, re.I):
return hit("plan.buckets", {"mode": "HYBRID", "capacityMode": "INFINITE", "includeForecast": True})
if re.search(r"有限产能粗|有限粗(能力|评|估)|粗能力评估|RCCP|有限RCCP", t, re.I):
return hit("plan.buckets", {"mode": "HYBRID", "capacityMode": "FINITE", "includeForecast": True})
# PL-01 时间分桶(须在「试排」等动词前,避免「周计划」误触排产)
if re.search(r"时间分桶|分桶计划|计划分桶|混合分桶|查看分桶|打开分桶|"
r"周计划表|月计划表|日计划桶|粗能力分桶|计划层分桶", t):
params: dict[str, Any] = {"mode": "HYBRID", "includeForecast": True, "capacityMode": "FINITE"}
if re.search(r"混合", t):
params["mode"] = "HYBRID"
elif re.search(r"月", t):
params["mode"] = "MONTH"
elif re.search(r"周", t):
params["mode"] = "WEEK"
elif re.search(r"日|天", t):
params["mode"] = "DAY"
if re.search(r"不含预测|不要预测|仅确定|只要订单", t):
params["includeForecast"] = False
if re.search(r"无限", t):
params["capacityMode"] = "INFINITE"
return hit("plan.buckets", params)
# SC-08 战役合并预览 / 试排(须在换型最小化与通用试排之前)
if re.search(r"战役(合并)?预览|批次(合并)?预览|合并预览|看(一下)?战役", t):
params: dict[str, Any] = {"windowDays": 7}
m_w = re.search(r"(\d+)\s*天", t)
if m_w:
params["windowDays"] = int(m_w.group(1))
return hit("campaign.preview", params)
if re.search(r"战役合并|批次合并|同品合并|合并战役|合并批次", t):
params = {"strategy": "CAMPAIGN", "engine": "HYBRID"}
if re.search(r"规则", t):
params["engine"] = "RULE"
elif re.search(r"约束规划|CP-?SAT|\bCP\b", t, re.I):
params["engine"] = "CP"
if re.search(r"预测", t):
params["includeForecast"] = True
return hit("schedule.run", params)
# SC-07 换型最小化试排(须在通用「试排」与「产能均衡」之前)
if re.search(r"换型最小化|减少换型|换型优先|最小化换型|少换型", t):
params: dict[str, Any] = {"strategy": "CHANGEOVER_MIN", "engine": "HYBRID"}
if re.search(r"规则", t):
params["engine"] = "RULE"
elif re.search(r"约束规划|CP-?SAT|\bCP\b", t, re.I):
params["engine"] = "CP"
if re.search(r"预测", t):
params["includeForecast"] = True
return hit("schedule.run", params)
# 执行排产:要求"动词形态"命中(试排/重排/排一版/执行|开始|重新排产),
# 避免仅出现"排产"名词的闲聊被误判(该类输入应交给二级 LLM 判定)
if re.search(r"预测纳入试排|含预测试排|带预测试排|试排.*预测|预测.*试排", t):
return hit("schedule.run", {"includeForecast": True, "engine": "HYBRID"})
if re.search(r"试排|重排|排一版|(执行|开始|重新|跑一?版?)排产", t):
strategy = ("KITTING_FIRST" if re.search(r"齐套优先|优先齐套|按齐套", t)
else "SKILL_FIRST" if re.search(r"技能优先|优先技能|按技能", t)
else "DELIVERY_FIRST" if re.search(r"交期", t)
else "CAPACITY_BALANCE" if re.search(r"均衡|产能", t)
else "CAMPAIGN" if re.search(r"战役|批次|合并", t)
else "CHANGEOVER_MIN" 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 = None
if re.search(r"规则", t):
engine = "RULE"
elif re.search(r"约束规划|CP-?SAT|\bCP\b", t, re.I):
engine = "CP"
elif re.search(r"遗传|\bGA\b", t, re.I):
engine = "GA"
elif re.search(r"混合|\bHYBRID\b", t, re.I):
engine = "HYBRID"
params: dict = {}
if engine:
params["engine"] = engine
if strategy: # 用户明确点名才带策略槽位
params["strategy"] = strategy
if re.search(r"预测", t):
params["includeForecast"] = True
return hit("schedule.run", params)
if re.search(r"资源利用率|设备利用率|班组(负荷|并发)|利用率视口|打开利用率|利用率分析", t, re.I):
return hit("viewport.mode", {"mode": "util"})
if re.search(r"分桶视口|打开计划层|计划分桶视口", t):
return hit("viewport.mode", {"mode": "plan"})
if re.search(r"热力|负荷图|负荷热力", t): # 负荷热力图(避免单独「负荷」抢利用率)
return hit("viewport.mode", {"mode": "load"})
if re.search(r"KPI\s*仪表盘|运营仪表盘|打开仪表盘", t, re.I):
return hit("query.kpi")
if re.search(r"方案对比表|打开对比表|对比表视口", t):
return hit("scenario.compare")
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|利用率|仪表盘|(KPI|指标|利用率).{0,4}怎么样", t, re.I): # KPI 查询 / EX-08
return hit("query.kpi")
# SC-04 约束配置
if re.search(r"恢复默认约束|重置约束(配置|剖面)?", t):
return hit("constraint.profile.save", {"resetDefaults": True})
if re.search(r"关闭物料齐套|不管物料|先不管齐套|禁用齐套约束", t):
return hit("constraint.profile.save", {
"constraints": {"C6_material_kit": {"enabled": False}}})
if re.search(r"打开物料齐套|启用齐套约束|开启物料齐套", t):
return hit("constraint.profile.save", {
"constraints": {"C6_material_kit": {"enabled": True}}})
if re.search(r"齐套改(为|成)?软约束|物料齐套软约束", t):
return hit("constraint.profile.save", {
"constraints": {"C6_material_kit": {"enabled": True, "kind": "soft"}}})
if re.search(r"齐套改(为|成)?硬约束|物料齐套硬约束", t):
return hit("constraint.profile.save", {
"constraints": {"C6_material_kit": {"enabled": True, "kind": "hard"}}})
if re.search(r"打开约束配置|查看约束配置|约束剖面|约束中心|有哪些约束", t):
return hit("constraint.profile.query")
# OR-02 排产参数 / 客户等级权重
if re.search(r"恢复默认(排产)?参数|重置(客户)?等级权重|恢复权重默认", t):
return hit("params.update", {"resetDefaults": True})
m_vip_w = re.search(r"(提高|调高|设置|改|把).{0,8}VIP.{0,8}权重\s*(到|为|成|=)?\s*(\d+(?:\.\d+)?)", t, re.I)
if m_vip_w:
return hit("params.update", {"vipWeight": float(m_vip_w.group(3))})
if re.search(r"提高VIP权重|调高VIP权重|VIP权重提高", t, re.I):
return hit("params.update", {"vipWeight": 10.0})
if re.search(r"查看排产参数|当前(客户)?等级权重|排产权重|打开排产参数", t):
return hit("params.query")
if re.search(r"下一步建议|下一步做什么|该做什么|给我建议|主动引导|空态引导", t):
return hit("guidance.next")
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 做意图分类+槽位填充;输出非法/不可用一律降级为 assistant.reply。"""
raw = await get_provider().chat_json(_LLM_SYSTEM, text) # 请求 LLM(强制 JSON)
if raw is None: # LLM 不可用 → 通用助理(带上下文作答)
return IntentResult(intent="assistant.reply", params={"query": text},
confidence=0.9, source="LLM")
try:
raw.setdefault("params", {})
raw["source"] = "LLM"
result = IntentResult(**raw)
except ValidationError:
return IntentResult(intent="assistant.reply", params={"query": text},
confidence=0.85, source="LLM")
if result.confidence < 0.5 or result.intent == "unknown":
return IntentResult(intent="assistant.reply", params={"query": text},
confidence=0.85, source="LLM")
# 保证下游能拿到原话
if "query" not in result.params:
result.params["query"] = text
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 或通用助理