落地空运群内协同与锁舱出站。

群里按 BOT 通道激活当前工单、航线报价和成交跟进;没有 TMS 报价编号时用工单字段补齐仍调锁舱,避免被本地规则拦住。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 17:25:42 +08:00
co-authored by Cursor
parent 12219cacb1
commit d9029dec89
62 changed files with 6772 additions and 159 deletions
@@ -0,0 +1,184 @@
"""
空运协同群文字意图:激活 / 补含电含磁 / 航线报价 / 锁舱释放 / 成交。
本文件职责:只分类,不写主账、不发企微。
禁止:把「锁舱」写成唯一口令;海运解散不走这里。
"""
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_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_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 + 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
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=在补是否含电/是否含磁;"
"product_quote=航线在报价格或费用(含具体金额);"
"quote_confirm_tms=确认沿用当前TMS报价;"
"quote_keep_tms=要在已有TMS报价上改;"
"quote_drop_tms=不要TMS、只用刚报的价;"
"deal_won=销售确认成交;deal_lost=销售确认未成交;"
"deal_negotiate=销售要继续协商;"
"other=闲聊。"
"一句话里既有锁舱又有工单号,回 air_lock。"
"有金额就是 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 ""