# -*- coding: utf-8 -*- """订单 / 快递意图识别。 只有客户在问订单、快递、物流、审核这类问题时,才把当前会话上下文交给订单 MCP。 否则每条消息都拉起一个只读订单服务子进程,既浪费,也让模型在闲聊时多了一个 可以误调用的工具。 关键词可以在 ``ai_config.AI_ORDER_INTENT_KEYWORDS`` 覆盖;留空则使用内置列表。 """ from __future__ import annotations import re from typing import Any, Iterable DEFAULT_ORDER_KEYWORDS: tuple[str, ...] = ( "快递", "物流", "订单", "单号", "运单", "快件", "包裹", "发货", "发出", "寄出", "寄了", "寄过", "邮寄", "发了吗", "发没发", "到哪", "到了吗", "到货", "到没到", "什么时候到", "啥时候到", "几天到", "什么时候发", "啥时候发", "什么时候寄", "啥时候寄", "签收", "派送", "派件", "配送", "送到", "收到药", "药到", "药寄", "药发", "审核", "审核通过", "审没审", "过审", "支付审核", "处方审核", "顺丰", "京东", "圆通", "中通", "申通", "韵达", "邮政", "ems", "极兔", "德邦", "退款", "退货", "拒收", "改地址", "换地址", "地址写错", ) # 客户在被要求“确认是哪一笔”之后的短回复,本身不含订单关键词,但仍属于订单话题。 _FOLLOW_UP_RE = re.compile( r"^(第?[一二三两1-9]\s*(个|笔|单|次)?|最近(的|那)?(一)?(笔|个|单)?|上(一|次)(的|那)?(笔|个|单)?|" r"(那|这)(笔|个|单)|都|全部|两个都|\d{1,2}月\d{1,2}(号|日)?(的|那)?(笔|个|单)?|" r"[A-Za-z]{0,4}\d{6,}|(是|对|嗯|好)的?|不是|另一(个|笔))[。!!??~~,,\s]*$" ) def _keywords() -> tuple[str, ...]: try: import ai_config raw = getattr(ai_config, "AI_ORDER_INTENT_KEYWORDS", None) except Exception: raw = None if isinstance(raw, (list, tuple)): cleaned = tuple(str(item).strip().lower() for item in raw if str(item).strip()) if cleaned: return cleaned return DEFAULT_ORDER_KEYWORDS def _normalize(text: Any) -> str: return re.sub(r"\s+", "", str(text or "")).lower() def matches_order_keywords(text: Any, keywords: Iterable[str] | None = None) -> bool: body = _normalize(text) if not body: return False return any(word and word in body for word in (keywords or _keywords())) def _history_texts(history: Any, limit: int = 4) -> list[str]: if not isinstance(history, list): return [] texts = [] for item in history[-limit:]: if isinstance(item, dict): texts.append(str(item.get("content") or "")) else: texts.append(str(item or "")) return texts def is_order_intent(customer_text: Any, history: Any = None) -> bool: """当前这轮是否属于订单 / 快递话题。 直接命中关键词即为订单意图;否则只有在最近几轮里刚聊过订单、且客户这轮是 “第一个 / 9 月 18 号那个 / 单号”这类短回复时才算延续订单话题。 """ text = str(customer_text or "").strip() keywords = _keywords() if matches_order_keywords(text, keywords): return True if not text or len(_normalize(text)) > 24: return False if not _FOLLOW_UP_RE.match(text): return False return any(matches_order_keywords(item, keywords) for item in _history_texts(history)) def filter_context( mcp_context: dict[str, Any] | None, customer_text: Any, history: Any = None ) -> dict[str, Any]: """只在订单意图成立时保留会话上下文,否则返回空 dict(不挂载订单 MCP)。""" if not mcp_context or not isinstance(mcp_context, dict): return {} if not is_order_intent(customer_text, history): return {} return dict(mcp_context)