Files
inquiry_robot/inquiry-agent/agent/routing/group_intent.py
T
2026-09-20 17:52:19 +08:00

272 lines
8.2 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
海运协同群文字意图: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
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 == INTENT_DISSOLVE:
logger.warning("群意图模型把成交判成解散,改回 %s text=%s", 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:
return (text or "").replace(" ", "").replace(" ", "").lower()
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 ""