206 lines
6.7 KiB
Python
206 lines
6.7 KiB
Python
"""
|
|
意图:普通文字询价(空运 / 海运闭环)。
|
|
|
|
本文件职责:入站文字 → 流程编排;回复只写入 outbox。
|
|
禁止:在本文件抽字段语义(交给 extract);禁止直连 TMS;禁止回调线程外的额外阻塞。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any, Optional
|
|
|
|
from agent.channel.queue import get_message_store
|
|
from agent.channel.wecom.models import InboundMessage
|
|
from agent.config import get_settings
|
|
from agent.llm.extract_text import detect_transport_mode
|
|
from agent.policy.air_text_flow import get_air_text_flow
|
|
from agent.policy.land_text_flow import get_land_text_flow
|
|
from agent.policy.sea_text_flow import get_sea_text_flow
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _pick_text_flow(*, sender_id: str, text: str, injected_mode: str = ""):
|
|
"""
|
|
按当前书签或本句运输方式选空运/海运/陆运流程。
|
|
|
|
陆运核对卡上的确定优先于旧海运书签,避免「确定」被海运/空运吃掉。
|
|
"""
|
|
from agent.schema.land_options import looks_like_confirm
|
|
|
|
land = get_land_text_flow()
|
|
if looks_like_confirm(text):
|
|
restored = land.restore_confirm_session(sender_id)
|
|
if restored:
|
|
return land
|
|
retry = land.restore_tms_retry_session(sender_id)
|
|
if retry:
|
|
return land
|
|
sea = get_sea_text_flow()
|
|
sea_sess = sea.session_of(sender_id)
|
|
if sea_sess and (sea_sess.business_line or "").upper() == "SEA":
|
|
return sea
|
|
land_sess = land.session_of(sender_id)
|
|
if land_sess and (land_sess.business_line or "").upper() == "LAND":
|
|
return land
|
|
mode = (injected_mode or detect_transport_mode(text) or "").upper()
|
|
if mode == "SEA":
|
|
return sea
|
|
if mode == "LAND":
|
|
return land
|
|
return get_air_text_flow()
|
|
|
|
|
|
def is_pending_land_confirm(message: InboundMessage) -> bool:
|
|
"""
|
|
陆运核对卡已出、本句是确定查价。
|
|
|
|
测服实测「确定」两字会被 DeepSeek A 判成闲聊,必须在分发层拦住,
|
|
不能回首次问候、不能跳过建单。
|
|
"""
|
|
if not message.has_sender():
|
|
return False
|
|
from agent.schema.land_options import looks_like_confirm
|
|
|
|
if not looks_like_confirm(message.content or ""):
|
|
return False
|
|
sess = get_land_text_flow().restore_confirm_session(message.sender_id)
|
|
ok = bool(
|
|
sess
|
|
and (sess.business_line or "").upper() == "LAND"
|
|
and sess.phase == "wait_confirm"
|
|
)
|
|
if not ok:
|
|
logger.info(
|
|
"land.confirm miss sender=%s phase=%s line=%s",
|
|
message.sender_id,
|
|
getattr(sess, "phase", "-"),
|
|
getattr(sess, "business_line", "-"),
|
|
)
|
|
return ok
|
|
|
|
|
|
def is_pending_land_tms_retry(message: InboundMessage) -> bool:
|
|
"""
|
|
陆运工单已建、上次查价失败,本句是确定重查。
|
|
|
|
不能回首次问候、不能再开一张新工单。
|
|
"""
|
|
if not message.has_sender():
|
|
return False
|
|
from agent.schema.land_options import looks_like_confirm
|
|
|
|
if not looks_like_confirm(message.content or ""):
|
|
return False
|
|
sess = get_land_text_flow().restore_tms_retry_session(message.sender_id)
|
|
return bool(
|
|
sess
|
|
and (sess.business_line or "").upper() == "LAND"
|
|
and sess.phase == "tms_tech"
|
|
and (sess.work_order_no or "").strip()
|
|
)
|
|
|
|
|
|
def is_land_confirm_action(message: InboundMessage) -> bool:
|
|
"""核对卡确定建单,或查价失败后确定重查。"""
|
|
return is_pending_land_confirm(message) or is_pending_land_tms_retry(message)
|
|
|
|
|
|
def handle_text_inquiry(
|
|
message: InboundMessage,
|
|
*,
|
|
injected_facts: Optional[dict[str, Any]] = None,
|
|
injected_mode: str = "",
|
|
b_result: Optional[dict[str, Any]] = None,
|
|
chat_fn=None,
|
|
flow=None,
|
|
store=None,
|
|
) -> str:
|
|
"""
|
|
处理一条私聊文字询价。
|
|
|
|
须已有 sender_id。副作用:outbox 多条回复;主账经 flow.ledger。
|
|
返回:流程阶段名。
|
|
"""
|
|
if not message.has_sender():
|
|
logger.warning("text_inquiry 拒绝:无 sender_id")
|
|
return "reject_no_sender"
|
|
settings = get_settings()
|
|
msg_store = store or get_message_store()
|
|
engine = flow or _pick_text_flow(
|
|
sender_id=message.sender_id,
|
|
text=message.content or "",
|
|
injected_mode=injected_mode,
|
|
)
|
|
replies: list[str] = []
|
|
|
|
def reply(text: str, extra: dict | None = None) -> str:
|
|
# 销售询价正文不加 [test];[test] 只给运维告警
|
|
body = text
|
|
replies.append(body)
|
|
ok, info = msg_store.enqueue_outbound(
|
|
touser=message.sender_id,
|
|
content=body,
|
|
dedupe_key=f"inq:{message.message_id}:{len(replies)}",
|
|
payload=dict(extra or {}),
|
|
)
|
|
logger.info("text_inquiry 出站 ok=%s info=%s phase_reply=%s", ok, info, body[:80])
|
|
return info if ok else ""
|
|
|
|
phase = engine.on_text(
|
|
sender_id=message.sender_id,
|
|
text=message.content or "",
|
|
reply=reply,
|
|
injected_facts=injected_facts,
|
|
injected_mode=injected_mode,
|
|
b_result=b_result,
|
|
allow_b=bool(settings.llm_allow_network and settings.llm_data_usage_confirmed),
|
|
chat_fn=chat_fn,
|
|
inbound_raw=message.raw,
|
|
)
|
|
logger.info("text_inquiry phase=%s sender=%s", phase, message.sender_id)
|
|
return phase
|
|
|
|
|
|
def handle_text_other(message: InboundMessage, *, store=None) -> str:
|
|
"""
|
|
闲聊/其它:若有待点按钮则当按钮;否则回首次单聊话术。
|
|
"""
|
|
from agent.policy import inquiry_copy as copy
|
|
|
|
if not message.has_sender():
|
|
return "reject_no_sender"
|
|
msg_store = store or get_message_store()
|
|
engine = _pick_text_flow(
|
|
sender_id=message.sender_id, text=message.content or ""
|
|
)
|
|
sess = engine.session_for_action(
|
|
message.sender_id, message.raw, text=message.content or ""
|
|
)
|
|
btn = engine.match_button(message.content or "", sess)
|
|
if btn:
|
|
return handle_text_inquiry(message, store=msg_store)
|
|
if is_land_confirm_action(message):
|
|
logger.info("text_other 陆运确定/重查,改走询价流程 sender=%s", message.sender_id)
|
|
return handle_text_inquiry(message, store=msg_store)
|
|
|
|
from agent.schema.land_options import looks_like_confirm
|
|
|
|
if looks_like_confirm(message.content or ""):
|
|
body = copy.LAND_CONFIRM_EXPIRED
|
|
msg_store.enqueue_outbound(
|
|
touser=message.sender_id,
|
|
content=body,
|
|
dedupe_key=f"land-confirm-expired:{message.message_id}",
|
|
)
|
|
return "land_confirm_expired"
|
|
|
|
body = copy.FIRST_CHAT
|
|
msg_store.enqueue_outbound(
|
|
touser=message.sender_id,
|
|
content=body,
|
|
dedupe_key=f"other:{message.message_id}",
|
|
)
|
|
return "text_other"
|