aps-agent/server/agent_core/intent.py

175 lines
12 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产能均衡, 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 语义解析(含降级)