落地转人工:三次补问或明确接管后出入口,真正转人工后冻结本票并允许新开询价。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -30,6 +30,7 @@ from agent.policy.air_text_flow import get_air_text_flow
|
||||
from agent.policy.sea_group_ops import (
|
||||
capture_group_lost_reason,
|
||||
group_client_of,
|
||||
is_handoff_ticket,
|
||||
send_group_text,
|
||||
ticket_view,
|
||||
)
|
||||
@@ -112,6 +113,12 @@ def handle_air_group(
|
||||
return "duplicate_inbound"
|
||||
mark_air_text(chat_id=chat_id, sender_id=message.sender_id, text=text)
|
||||
wo = copy.extract_work_order_no(text)
|
||||
if wo:
|
||||
from agent.policy.handoff_human import try_commit_air_group_handoff
|
||||
|
||||
handed = try_commit_air_group_handoff(engine, chat_id=chat_id, work_order_no=wo)
|
||||
if handed:
|
||||
return handed
|
||||
intent = classify_air_group_text(text, injected=injected_intent)
|
||||
logger.info(
|
||||
"air_group 文字 chat=%s sender=%s intent=%s wo=%s",
|
||||
@@ -138,6 +145,9 @@ def handle_air_group(
|
||||
)
|
||||
if ticket is None:
|
||||
return phase
|
||||
if is_handoff_ticket(ticket):
|
||||
logger.info("air_group 转人工不再跟 chat=%s", chat_id)
|
||||
return "handoff_ignore"
|
||||
if intent == INTENT_ACTIVATE:
|
||||
return phase
|
||||
else:
|
||||
@@ -145,6 +155,9 @@ def handle_air_group(
|
||||
if ticket is None:
|
||||
send_group_text(group_client_of(engine), chat_id, copy.air_need_work_order())
|
||||
return "need_work_order"
|
||||
if is_handoff_ticket(ticket):
|
||||
logger.info("air_group 转人工不再跟 chat=%s", chat_id)
|
||||
return "handoff_ignore"
|
||||
return _handle_bound(
|
||||
engine,
|
||||
message,
|
||||
@@ -177,6 +190,9 @@ def _handle_file(engine, message: InboundMessage) -> str:
|
||||
if ticket is None:
|
||||
send_group_text(group_client_of(engine), chat_id, copy.air_need_work_order())
|
||||
return "need_work_order"
|
||||
if is_handoff_ticket(ticket):
|
||||
logger.info("air_group 转人工不再跟 chat=%s", chat_id)
|
||||
return "handoff_ignore"
|
||||
return apply_air_file(
|
||||
flow=engine,
|
||||
ticket=ticket,
|
||||
|
||||
@@ -22,6 +22,7 @@ from agent.policy.sea_group_ops import (
|
||||
group_client_of,
|
||||
handle_group_file,
|
||||
hydrate_ticket,
|
||||
is_handoff_ticket,
|
||||
peek_pending_product_quote,
|
||||
send_group_text,
|
||||
ticket_no,
|
||||
@@ -66,7 +67,7 @@ def handle_group_inbound(
|
||||
群里一条文字或附件。
|
||||
|
||||
文字走 DeepSeek A(单测可注入);附件只看扩展名。
|
||||
协同已结束或终态要回话,不能当没看见。
|
||||
已成交/未成交/已关闭要回话。真正转人工后群里不再跟、不回。
|
||||
"""
|
||||
if (message.chat_type or "") != "group":
|
||||
return "group_ignore"
|
||||
@@ -100,6 +101,13 @@ def handle_group_inbound(
|
||||
return "group_ignore"
|
||||
|
||||
sess = _session_of_ticket(engine, ticket)
|
||||
if is_handoff_ticket(ticket, sess):
|
||||
logger.info(
|
||||
"group_inbound 转人工不再跟 wo=%s chat=%s",
|
||||
ticket_no(ticket),
|
||||
chat_id,
|
||||
)
|
||||
return "handoff_ignore"
|
||||
view = ticket_view(ticket)
|
||||
if view.get("collab_ended") or (sess is not None and getattr(sess, "collab_ended", False)):
|
||||
from agent.policy import inquiry_copy as copy
|
||||
|
||||
@@ -22,9 +22,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _front_bookmark(sess) -> bool:
|
||||
"""补问/核对手稿优先于已出票旧书签。空的 need_mode 不算(那是丢会话后的误伤)。"""
|
||||
"""补问/核对手稿优先于已出票旧书签。空的 need_mode 不算(那是丢会话后的误伤)。已转人工的票也不再当当前询价。"""
|
||||
if sess is None:
|
||||
return False
|
||||
from agent.policy.handoff_human import is_handoff_frozen
|
||||
|
||||
if is_handoff_frozen(sess):
|
||||
return False
|
||||
phase = (sess.phase or "").strip()
|
||||
line = (sess.business_line or "").upper()
|
||||
if phase == "need_mode" and not line:
|
||||
@@ -32,16 +36,41 @@ def _front_bookmark(sess) -> bool:
|
||||
return phase in {"clarify", "need_mode", "need_land_options", "wait_confirm"}
|
||||
|
||||
|
||||
def should_resume_other_as_inquiry(message: InboundMessage) -> bool:
|
||||
"""
|
||||
A 把补问短答标成闲聊时,仍续填当前询价。
|
||||
|
||||
只拦还在问字段/选项/核对、未成交原因、转人工入口。
|
||||
已报价协同中不拦,避免「谢谢」被当成新询价。
|
||||
"""
|
||||
if not message.has_sender():
|
||||
return False
|
||||
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 ""
|
||||
)
|
||||
if sess is None:
|
||||
return False
|
||||
phase = (sess.phase or "").strip()
|
||||
if phase in {"wait_lost_reason", "handoff_offer"}:
|
||||
return True
|
||||
return _front_bookmark(sess)
|
||||
|
||||
|
||||
def _pick_text_flow(*, sender_id: str, text: str, injected_mode: str = "", prefer_quoted: bool = False):
|
||||
"""
|
||||
按当前书签或本句运输方式选空运/海运/陆运流程。
|
||||
|
||||
陆运核对卡上的确定优先于旧海运书签,避免「确定」被海运/空运吃掉。
|
||||
A 已判成调价时交给当前已报价工单,避免旧海运书签抢走。
|
||||
正在补问时,补问书签优先于已报价调价,避免第 3 次反问出不了转人工卡。
|
||||
A 已判成调价且当前不是补问:交给当前已报价工单,避免旧海运书签抢走。
|
||||
图片识图在 Worker 写下的补问书签,HTTP 必须从 Redis 读到,否则「托盘」会变成再问运输方式。
|
||||
"""
|
||||
from agent.schema.land_options import looks_like_confirm
|
||||
from agent.policy.quote_adjust_ops import quoted_adjust_flow
|
||||
from agent.policy.handoff_human import is_handoff_frozen
|
||||
|
||||
land = get_land_text_flow()
|
||||
if looks_like_confirm(text):
|
||||
@@ -51,21 +80,22 @@ def _pick_text_flow(*, sender_id: str, text: str, injected_mode: str = "", prefe
|
||||
retry = land.restore_tms_retry_session(sender_id)
|
||||
if retry:
|
||||
return land
|
||||
if prefer_quoted:
|
||||
quoted = quoted_adjust_flow(sender_id)
|
||||
if quoted is not None:
|
||||
return quoted
|
||||
sea = get_sea_text_flow()
|
||||
air = get_air_text_flow()
|
||||
sea_sess = sea.session_of(sender_id)
|
||||
land_sess = land.session_of(sender_id)
|
||||
air_sess = air.session_of(sender_id)
|
||||
# 补问/核对手稿优先于「已报价调价」,避免第 3 次反问被调价路由冲掉、出不了转人工卡。
|
||||
for sess, flow in ((land_sess, land), (air_sess, air), (sea_sess, sea)):
|
||||
if _front_bookmark(sess):
|
||||
return flow
|
||||
if sea_sess and (sea_sess.business_line or "").upper() == "SEA":
|
||||
if prefer_quoted:
|
||||
quoted = quoted_adjust_flow(sender_id)
|
||||
if quoted is not None:
|
||||
return quoted
|
||||
if sea_sess and not is_handoff_frozen(sea_sess) and (sea_sess.business_line or "").upper() == "SEA":
|
||||
return sea
|
||||
if land_sess and (land_sess.business_line or "").upper() == "LAND":
|
||||
if land_sess and not is_handoff_frozen(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":
|
||||
@@ -140,6 +170,7 @@ def handle_text_inquiry(
|
||||
flow=None,
|
||||
store=None,
|
||||
prefer_quoted: bool = False,
|
||||
prefer_handoff: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
处理一条私聊文字询价。
|
||||
@@ -177,6 +208,15 @@ def handle_text_inquiry(
|
||||
logger.info("text_inquiry 出站 ok=%s info=%s phase_reply=%s", ok, info, body[:80])
|
||||
return info if ok else ""
|
||||
|
||||
if prefer_handoff:
|
||||
phase = engine.offer_handoff(
|
||||
sender_id=message.sender_id,
|
||||
text=message.content or "",
|
||||
reply=reply,
|
||||
)
|
||||
logger.info("text_inquiry handoff phase=%s sender=%s", phase, message.sender_id)
|
||||
return phase
|
||||
|
||||
phase = engine.on_text(
|
||||
sender_id=message.sender_id,
|
||||
text=message.content or "",
|
||||
@@ -194,21 +234,26 @@ def handle_text_inquiry(
|
||||
|
||||
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()
|
||||
if should_resume_other_as_inquiry(message):
|
||||
logger.info(
|
||||
"text_other 续填当前询价 sender=%s chars=%s",
|
||||
message.sender_id,
|
||||
len((message.content or "").strip()),
|
||||
)
|
||||
return handle_text_inquiry(message, store=msg_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 ""
|
||||
)
|
||||
if sess and sess.phase == "wait_lost_reason":
|
||||
return handle_text_inquiry(message, store=msg_store)
|
||||
btn = engine.match_button(message.content or "", sess)
|
||||
if btn:
|
||||
return handle_text_inquiry(message, store=msg_store)
|
||||
|
||||
Reference in New Issue
Block a user