""" 空运协同群文字意图:激活 / 补含电含磁 / 航线报价 / 锁舱释放 / 成交。 本文件职责:只分类,不写主账、不发企微。 禁止:把「锁舱」写成唯一口令;海运解散不走这里。 """ from __future__ import annotations import logging from typing import Optional from agent.policy import inquiry_copy as copy from agent.routing.group_intent import ( INTENT_CONFIRM_TMS, INTENT_DEAL, INTENT_DROP_TMS, INTENT_FIELDS, INTENT_KEEP_TMS, INTENT_ADJUST, INTENT_LOST, INTENT_NEGOTIATE, INTENT_OTHER, INTENT_QUOTE, _hint_confirm_tms, _hint_deal_outcome, _hint_tms_choice, ) logger = logging.getLogger(__name__) INTENT_ACTIVATE = "air_activate" INTENT_LOCK = "air_lock" INTENT_RELEASE = "air_release" INTENT_PICK_OPTION = "air_pick_option" _VALID = { INTENT_ACTIVATE, INTENT_LOCK, INTENT_RELEASE, INTENT_PICK_OPTION, INTENT_FIELDS, INTENT_QUOTE, INTENT_ADJUST, INTENT_KEEP_TMS, INTENT_DROP_TMS, INTENT_CONFIRM_TMS, INTENT_DEAL, INTENT_LOST, INTENT_NEGOTIATE, INTENT_OTHER, } # 相近说法回退,不是唯一口令。 _LOCK_HINTS = ("锁舱", "锁仓", "锁定舱位", "锁住舱位", "帮我锁舱") _RELEASE_HINTS = ("释放舱位", "放舱", "解锁舱", "取消锁舱", "释放舱") def is_missed_air_bot_at(text: str, *, room_already_air: bool = False) -> bool: """ 存档里看到用户已经 @ 询价小助手。BOT 长连接没推到时,用这一刀补听。 海运拉人卡片也带「询价小助手」,必须排除。闲聊没有 @ 仍不当业务。 """ raw = (text or "").strip() if "询价小助手" not in raw: return False if "运输方式:海运" in raw or "运输方式:海运" in raw: return False wo = copy.extract_work_order_no(raw) if wo: compact = "".join(raw.split()) return len(compact) < 80 return bool(room_already_air) def classify_air_group_text( text: str, *, injected: str = "", allow_network: Optional[bool] = None, ) -> str: """ 空运群一句文字的意图。单测可注入;正式走模型。 锁舱/释放优先于成交,避免「锁舱成交」误伤。 """ hint = (injected or "").strip() if hint in _VALID: return hint raw = (text or "").strip() if not raw: return INTENT_OTHER locked = _hint_lock_or_release(raw) if locked: return locked # wo_plus_deal_not_activate:工单号+成交跟进同一句先关单,不能只当激活。 closed = _hint_deal_outcome(raw) if copy.extract_work_order_no(raw) and closed: return closed # 只发工单号先当激活,不要先打模型;WO + 12 位数字也会被费用行误认成金额。 if copy.extract_work_order_no(raw) and _only_work_order(raw): return INTENT_ACTIVATE try: from agent.config import get_settings settings = get_settings() use_net = ( allow_network if allow_network is not None else bool(getattr(settings, "llm_allow_network", False)) ) if use_net and getattr(settings, "llm_data_usage_confirmed", False): got = _classify_with_model(raw, settings) if got in _VALID: return got except Exception: logger.exception("空运群意图模型失败,回退规则") from agent.llm.extract_text import extract_air_collab_fields, extract_product_quote from agent.schema.quote_adjust import extract_adjust if extract_adjust(raw).is_adjust: return INTENT_ADJUST if extract_product_quote(raw): return INTENT_QUOTE confirmed = _hint_confirm_tms(raw) if confirmed: return confirmed hinted = _hint_tms_choice(raw) if hinted: return hinted closed = _hint_deal_outcome(raw) if closed: return closed if extract_air_collab_fields(raw): return INTENT_FIELDS if _looks_like_option(raw): return INTENT_PICK_OPTION return INTENT_OTHER def _hint_lock_or_release(text: str) -> str: blob = (text or "").replace(" ", "").replace(" ", "") if any(x in blob for x in _RELEASE_HINTS): return INTENT_RELEASE if any(x in blob for x in _LOCK_HINTS): return INTENT_LOCK return "" def _only_work_order(text: str) -> bool: """去掉工单号和 @询价小助手 后几乎没字,当作激活/切单。""" no = copy.extract_work_order_no(text) rest = (text or "").replace(no, "").replace(no.lower(), "") rest = rest.replace("@询价小助手", "").replace("询价小助手", "") rest = "".join(ch for ch in rest if ch.isalnum() or "\u4e00" <= ch <= "\u9fff") return not rest.strip() def _looks_like_option(text: str) -> bool: blob = (text or "").replace(" ", "").upper() return blob.startswith("AIR-OPT") or blob.startswith("AIROPT") def _classify_with_model(text: str, settings) -> str: from agent.llm.http_client import LlmHttpClient from agent.llm.modes import LlmMode client = LlmHttpClient.from_settings(settings) system = ( "你是空运协同群意图分类。只回复一个英文词,不要解释。" "air_lock=航线要锁舱;air_release=航线要释放舱位;" "air_activate=只是指定/切换工单号;" "air_pick_option=在选舱位选项编号;" "collab_fields=在补是否含电/是否含磁;" "quote_adjust=销售在调已有费用,口令不写死(上升、往上抬、加一成、改到某金额都算),不是新产品报价;" "product_quote=航线在报价格或费用(含具体金额);" "quote_confirm_tms=确认沿用当前TMS报价;" "quote_keep_tms=要在已有TMS报价上改;" "quote_drop_tms=不要TMS、只用刚报的价;" "deal_won=销售确认成交;deal_lost=销售确认未成交;" "deal_negotiate=销售要继续协商;" "other=闲聊。" "一句话里既有锁舱又有工单号,回 air_lock。" "一句话里既有工单号又确认成交/未成交/协商中,回 deal_won/deal_lost/deal_negotiate,不要回 air_activate。" "改已有费用优先 quote_adjust;新报价格才是 product_quote。" ) data = client.chat( mode=LlmMode.ORDERED_ACTIONS, messages=[ {"role": "system", "content": system}, {"role": "user", "content": text[:500]}, ], ) got = str((data or {}).get("content") or "").strip().split()[0] if data else "" return got if got in _VALID else ""