""" 海运协同群文字意图:DeepSeek A 判断补字段 / 产品报价 / 解散 / 其它。 本文件职责:只分类,不写主账、不发企微。 禁止:把「解散群」写成唯一口令;私聊按钮不走这里。 """ from __future__ import annotations import logging from typing import Optional from agent.llm.extract_text import extract_collab_fields logger = logging.getLogger(__name__) INTENT_FIELDS = "collab_fields" INTENT_QUOTE = "product_quote" INTENT_ADJUST = "quote_adjust" INTENT_DISSOLVE = "dissolve" INTENT_KEEP_TMS = "quote_keep_tms" INTENT_DROP_TMS = "quote_drop_tms" INTENT_CONFIRM_TMS = "quote_confirm_tms" INTENT_DEAL = "deal_won" INTENT_LOST = "deal_lost" INTENT_NEGOTIATE = "deal_negotiate" INTENT_OTHER = "other" _VALID = { INTENT_FIELDS, INTENT_QUOTE, INTENT_ADJUST, INTENT_DISSOLVE, INTENT_KEEP_TMS, INTENT_DROP_TMS, INTENT_CONFIRM_TMS, INTENT_DEAL, INTENT_LOST, INTENT_NEGOTIATE, INTENT_OTHER, } # 口令不写死:这两句是建议说法,相近意思也认。有金额的仍当报价。 # 问句列了 1/2 时,产品常回「第一个」而不是整句口令。 _KEEP_HINTS = ( "在tms基础上", "在tms上改", "保留tms", "按tms改", "按tms的改", "留着tms", "在原来基础上", "在原报价上", "在原基础上", ) _KEEP_CHOICE = { "1", "1.", "1、", "一", "第一", "第一个", "第一条", "选项1", "选项一", "选1", "选一", "第1", "第1个", } _DROP_CHOICE = { "2", "2.", "2、", "二", "第二", "第二个", "第二条", "选项2", "选项二", "选2", "选二", "第2", "第2个", } # 先认未成交,避免「未成交」被「成交」误伤。 _LOST_HINTS = ("未成交", "没成交", "没有成交", "没做成", "黄了") _NEGOTIATE_HINTS = ("协商中", "继续谈", "再谈谈", "还在谈") _DEAL_HINTS = ("成交", "成了", "做成了", "已经成交", "搞定了") _CONFIRM_CHOICE = { "确认报价", "确认该报价", "确认这个报价", "确认tms报价", "沿用该价", "沿用tms", "沿用tms报价", } _CONFIRM_HINTS = ( "确认报价", "确认该报价", "确认这个报价", "确认tms报价", "沿用该价", "沿用tms", ) _DROP_HINTS = ( "不用tms报价", "不要tms报价", "不用tms", "不要tms", "不要tms的价", "按我的报价", "按我刚才报", "只用我的", "全部按我", ) def classify_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 # 问句让回 1 或 2。整句就是选项时先认,不能把「2」抽成价格。 exact = _exact_tms_choice(raw) if exact: return exact 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: hinted_confirm = _hint_confirm_tms(raw) if hinted_confirm and got in {INTENT_OTHER, INTENT_FIELDS}: logger.info("群意图模型判成%s,按口令改回 %s text=%s", got, hinted_confirm, raw[:40]) return hinted_confirm hinted_tms = _hint_tms_choice(raw) if hinted_tms and got in {INTENT_OTHER, INTENT_FIELDS}: logger.info("群意图模型判成%s,按口令改回 %s text=%s", got, hinted_tms, raw[:40]) return hinted_tms hinted_deal = _hint_deal_outcome(raw) # 销售说成交时,模型不能把群拆了,也不能把「工单号+未成交」判成闲聊。 if hinted_deal and got in {INTENT_DISSOLVE, INTENT_OTHER, INTENT_FIELDS}: logger.warning("群意图模型把成交判成%s,改回 %s text=%s", got, hinted_deal, raw[:40]) return hinted_deal return got except Exception: logger.exception("群意图模型失败,回退标签字段") from agent.llm.extract_text import 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_collab_fields(raw): return INTENT_FIELDS return INTENT_OTHER def _norm_intent_text(text: str) -> str: """去掉空格和 @询价小助手,方便整句就是「2」时对上选项。""" blob = (text or "").replace(" ", "").replace(" ", "").lower() blob = blob.replace("@询价小助手", "").replace("询价小助手", "") return blob.strip() def _exact_tms_choice(text: str) -> str: """整句只是 1 / 2 / 第一个 / 第二个。这是在选题,不是报价金额。""" blob = _norm_intent_text(text) if blob in _KEEP_CHOICE: return INTENT_KEEP_TMS if blob in _DROP_CHOICE: return INTENT_DROP_TMS return "" def _hint_confirm_tms(text: str) -> str: """沿用当前 TMS 价。口令不写死,确认报价 / 沿用该价都认。""" blob = _norm_intent_text(text) if blob in _CONFIRM_CHOICE: return INTENT_CONFIRM_TMS if any(x in blob for x in _CONFIRM_HINTS): return INTENT_CONFIRM_TMS return "" def _hint_tms_choice(text: str) -> str: """相近口令回退。有价格的句子不会走到这里。整句是 1/第一个 也认。""" blob = _norm_intent_text(text) if blob in _KEEP_CHOICE: return INTENT_KEEP_TMS if blob in _DROP_CHOICE: return INTENT_DROP_TMS if any(x in blob for x in _KEEP_HINTS): return INTENT_KEEP_TMS if any(x in blob for x in _DROP_HINTS): return INTENT_DROP_TMS return "" def _hint_deal_outcome(text: str) -> str: """成交/未成交/协商中:建议说法 + 相近口语。""" blob = _norm_intent_text(text) if any(x in blob for x in _LOST_HINTS): return INTENT_LOST if any(x in blob for x in _NEGOTIATE_HINTS): return INTENT_NEGOTIATE if any(x in blob for x in _DEAL_HINTS): return INTENT_DEAL return "" def _classify_with_model(text: str, settings) -> str: """DeepSeek 只回一个意图词。失败空串。""" from agent.llm.http_client import LlmHttpClient from agent.llm.modes import LlmMode client = LlmHttpClient.from_settings(settings) system = ( "你是海运协同群意图分类。只回复一个英文词,不要解释。" "collab_fields=在补货好时间/商品海关编码/含油含电含磁;" "quote_adjust=销售在调已有费用,口令不写死(上升、上浮、往上抬、加一成、减、改到某金额都算),不是新产品报价;" "product_quote=产品在报价格或费用(含具体金额);" "quote_confirm_tms=产品确认沿用当前TMS报价,没有报新金额;" "quote_keep_tms=要在已有TMS报价上改,没有报新金额;" "quote_drop_tms=不要TMS、只用产品刚报的价,没有报新金额;" "deal_won=销售确认这单成交了;" "deal_lost=销售确认这单未成交;" "deal_negotiate=销售要继续协商;" "dissolve=要把这个协同群解散;" "other=闲聊、问候、对询价港口的随口更正(不当改单)。" "一句话里既补字段又报价,回 product_quote。" "有金额就是 product_quote,不要判成 quote_keep_tms。" ) data = client.chat( mode=LlmMode.ORDERED_ACTIONS, messages=[ {"role": "system", "content": system}, {"role": "user", "content": text[:2000]}, ], ) content = str(getattr(data, "content", "") or "").strip().lower() for token in ( INTENT_ADJUST, INTENT_QUOTE, INTENT_CONFIRM_TMS, INTENT_KEEP_TMS, INTENT_DROP_TMS, INTENT_LOST, INTENT_NEGOTIATE, INTENT_DEAL, INTENT_DISSOLVE, INTENT_FIELDS, INTENT_OTHER, ): if token in content: return token return ""