# ============================================================ # 意图识别两级管线(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 或通用助理