落地转人工:三次补问或明确接管后出入口,真正转人工后冻结本票并允许新开询价。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -54,6 +54,12 @@ class FlowSession:
|
||||
deal_version: int = 0
|
||||
collab_ended: bool = False
|
||||
pending_product_quote: dict[str, Any] = field(default_factory=dict)
|
||||
clarify_missing_sig: str = ""
|
||||
clarify_retry: int = 0
|
||||
# clarify=3 次反问入口;intent=销售要求人来接管。空=没有待点的转人工卡。
|
||||
handoff_offer: str = ""
|
||||
handoff_committed: bool = False
|
||||
virtual_work_order_no: str = ""
|
||||
|
||||
|
||||
def _session_payload(sess: FlowSession) -> dict[str, Any]:
|
||||
@@ -91,6 +97,11 @@ def _session_from_payload(raw: dict[str, Any]) -> FlowSession:
|
||||
deal_version=int(raw.get("deal_version") or 0),
|
||||
collab_ended=bool(raw.get("collab_ended")),
|
||||
pending_product_quote=dict(raw.get("pending_product_quote") or {}),
|
||||
clarify_missing_sig=str(raw.get("clarify_missing_sig") or ""),
|
||||
clarify_retry=int(raw.get("clarify_retry") or 0),
|
||||
handoff_offer=str(raw.get("handoff_offer") or ""),
|
||||
handoff_committed=bool(raw.get("handoff_committed")),
|
||||
virtual_work_order_no=str(raw.get("virtual_work_order_no") or ""),
|
||||
)
|
||||
|
||||
|
||||
@@ -191,8 +202,199 @@ class AirTextInquiryFlow:
|
||||
no = (sess.work_order_no or "").strip()
|
||||
if no:
|
||||
self._by_ticket[no] = sess
|
||||
vt = (sess.virtual_work_order_no or "").strip()
|
||||
if vt:
|
||||
self._by_ticket[vt] = sess
|
||||
self._persist_bookmark(sess)
|
||||
|
||||
def _handoff_state(self, sess: Optional[FlowSession]) -> dict[str, Any]:
|
||||
"""新建书签时带上转人工计数,避免补问一轮就把 3 次清掉。已冻结的票不当新书签底。"""
|
||||
from agent.policy.handoff_human import is_handoff_frozen
|
||||
|
||||
if sess is None or is_handoff_frozen(sess):
|
||||
return {}
|
||||
return {
|
||||
"clarify_missing_sig": sess.clarify_missing_sig,
|
||||
"clarify_retry": sess.clarify_retry,
|
||||
"handoff_offer": sess.handoff_offer,
|
||||
"handoff_committed": sess.handoff_committed,
|
||||
"virtual_work_order_no": sess.virtual_work_order_no,
|
||||
"collab_chat_id": sess.collab_chat_id,
|
||||
"work_order_no": sess.work_order_no,
|
||||
"status": sess.status,
|
||||
"quote": dict(sess.quote or {}),
|
||||
"tms_quote": dict(sess.tms_quote or {}),
|
||||
"allowed": sess.allowed,
|
||||
"invited_userids": sess.invited_userids,
|
||||
"invited_sales_name": sess.invited_sales_name,
|
||||
"invited_product_names": sess.invited_product_names,
|
||||
}
|
||||
|
||||
def _block_if_handoff_committed(self, sess: Optional[FlowSession], reply: ReplyFn) -> bool:
|
||||
"""真正转人工之后这一票不再跟。返回 True 表示已经拦住。"""
|
||||
from agent.policy.handoff_human import is_handoff_frozen
|
||||
|
||||
if sess is None:
|
||||
return False
|
||||
if is_handoff_frozen(sess):
|
||||
no = (sess.work_order_no or sess.virtual_work_order_no or "").strip()
|
||||
reply(f"工单 {no} 已转人工,本单不再自动跟进。如需新询价请直接说明新的运输需求。")
|
||||
return True
|
||||
return False
|
||||
|
||||
def _detach_frozen_bookmark(
|
||||
self, sess: Optional[FlowSession], sender_id: str
|
||||
) -> Optional[FlowSession]:
|
||||
"""
|
||||
真正转人工后丢掉当前私聊书签,好开新询价。
|
||||
|
||||
旧单仍留在工单号索引,群里带号仍能对上。
|
||||
"""
|
||||
from agent.policy.handoff_human import is_handoff_frozen
|
||||
|
||||
if sess is None or not is_handoff_frozen(sess):
|
||||
return sess
|
||||
logger.info(
|
||||
"handoff.detach_frozen sender=%s wo=%s vt=%s",
|
||||
sender_id,
|
||||
sess.work_order_no or "-",
|
||||
sess.virtual_work_order_no or "-",
|
||||
)
|
||||
self.drop_current_bookmark(sender_id)
|
||||
return None
|
||||
|
||||
def offer_handoff(self, *, sender_id: str, text: str, reply: ReplyFn) -> str:
|
||||
"""
|
||||
路由判定销售明确要求人来接管。运输方式不明则继续补问运输方式。
|
||||
"""
|
||||
from agent.llm.extract_text import detect_transport_mode
|
||||
from agent.policy.handoff_human import is_closed_for_handoff
|
||||
|
||||
sess = self.session_of(sender_id)
|
||||
if sess and is_closed_for_handoff(sess.status):
|
||||
reply("该工单已结束,不能再转人工。")
|
||||
return "handoff_closed"
|
||||
if sess and self._block_if_handoff_committed(sess, reply):
|
||||
return "handoff_done"
|
||||
mode = (sess.business_line if sess else "") or detect_transport_mode(text) or ""
|
||||
mode = (mode or "").upper()
|
||||
if not mode:
|
||||
reply(copy.ASK_TRANSPORT)
|
||||
self._save(
|
||||
FlowSession(
|
||||
sender_id=sender_id,
|
||||
thread_id=sender_id,
|
||||
phase="need_mode",
|
||||
facts=dict(sess.facts) if sess else {},
|
||||
immutable_text=(sess.immutable_text if sess and sess.immutable_text else text),
|
||||
**self._handoff_state(sess),
|
||||
)
|
||||
)
|
||||
return "need_mode"
|
||||
if mode == "SEA":
|
||||
from agent.policy.sea_text_flow import get_sea_text_flow
|
||||
|
||||
return get_sea_text_flow()._offer_handoff_now(
|
||||
sender_id=sender_id, reason="intent", reply=reply, text=text
|
||||
)
|
||||
if mode == "LAND":
|
||||
from agent.policy.land_text_flow import get_land_text_flow
|
||||
|
||||
return get_land_text_flow()._offer_handoff_now(
|
||||
sender_id=sender_id, reason="intent", reply=reply, text=text
|
||||
)
|
||||
return self._offer_handoff_now(
|
||||
sender_id=sender_id, reason="intent", reply=reply, text=text
|
||||
)
|
||||
|
||||
def _offer_handoff_now(
|
||||
self,
|
||||
*,
|
||||
sender_id: str,
|
||||
reason: str,
|
||||
reply: ReplyFn,
|
||||
text: str = "",
|
||||
sess: Optional[FlowSession] = None,
|
||||
) -> str:
|
||||
"""
|
||||
发出转人工入口。reason=clarify|intent。
|
||||
点卡/空运群发号之前不冻结。
|
||||
"""
|
||||
from agent.policy.handoff_human import (
|
||||
has_collab_group,
|
||||
ticket_no_for_handoff_offer,
|
||||
transport_label,
|
||||
)
|
||||
|
||||
sess = sess or self.session_of(sender_id)
|
||||
if sess is None:
|
||||
sess = FlowSession(
|
||||
sender_id=sender_id,
|
||||
thread_id=sender_id,
|
||||
phase="handoff_offer",
|
||||
business_line=self.bookmark_kind.upper() if self.bookmark_kind != "sea" else "SEA",
|
||||
immutable_text=text,
|
||||
)
|
||||
if self.bookmark_kind == "air":
|
||||
sess.business_line = "AIR"
|
||||
elif self.bookmark_kind == "land":
|
||||
sess.business_line = "LAND"
|
||||
else:
|
||||
sess.business_line = "SEA"
|
||||
line = (sess.business_line or "").upper()
|
||||
if has_collab_group(sess):
|
||||
reply(copy.HANDOFF_ALREADY_COLLAB)
|
||||
return "handoff_already"
|
||||
why = copy.HANDOFF_CLARIFY_REASON if reason == "clarify" else copy.HANDOFF_INTENT_REASON
|
||||
if line == "AIR":
|
||||
no = ticket_no_for_handoff_offer(sess)
|
||||
reply(copy.handoff_air_hint(work_order_no=no))
|
||||
sess.handoff_offer = reason
|
||||
sess.phase = "handoff_offer"
|
||||
self._save(sess)
|
||||
return "handoff_offer"
|
||||
token = (sess.work_order_no or "").strip() or sender_id
|
||||
reply(
|
||||
copy.handoff_offer_text(reason=why, transport_mode=transport_label(line)),
|
||||
copy.handoff_offer_wecom_payload(reason=why, task_token=token),
|
||||
)
|
||||
sess.handoff_offer = reason
|
||||
sess.phase = "handoff_offer"
|
||||
sess.allowed = (copy.BTN_PULL_COLLAB,)
|
||||
self._save(sess)
|
||||
return "handoff_offer"
|
||||
|
||||
def _clarify_or_handoff(
|
||||
self,
|
||||
*,
|
||||
sess: Optional[FlowSession],
|
||||
keys: list[str],
|
||||
new_sess: FlowSession,
|
||||
reply: ReplyFn,
|
||||
clarify_text: str,
|
||||
) -> str:
|
||||
"""缺项:第一次不计数;满 3 次先发(3/3)再出转人工入口,否则照常补问。"""
|
||||
from agent.policy.handoff_human import inherit_pre_create_ticket, note_clarify_round
|
||||
|
||||
if sess is not None:
|
||||
new_sess.clarify_missing_sig = sess.clarify_missing_sig
|
||||
new_sess.clarify_retry = sess.clarify_retry
|
||||
new_sess.handoff_offer = sess.handoff_offer
|
||||
new_sess.handoff_committed = sess.handoff_committed
|
||||
new_sess.virtual_work_order_no = sess.virtual_work_order_no
|
||||
inherit_pre_create_ticket(sess, new_sess)
|
||||
decision = note_clarify_round(new_sess, keys)
|
||||
reply(copy.with_clarify_progress(clarify_text, new_sess.clarify_retry))
|
||||
if decision == "handoff":
|
||||
return self._offer_handoff_now(
|
||||
sender_id=new_sess.sender_id,
|
||||
reason="clarify",
|
||||
reply=reply,
|
||||
sess=new_sess,
|
||||
)
|
||||
self._save(new_sess)
|
||||
return new_sess.phase or "clarify"
|
||||
|
||||
def drop_current_bookmark(self, sender_id: str) -> Optional[FlowSession]:
|
||||
"""
|
||||
发图新开:丢掉当前书签,已出号会话仍留在 _by_ticket。
|
||||
@@ -467,6 +669,7 @@ class AirTextInquiryFlow:
|
||||
if not (sender_id or "").strip():
|
||||
return "reject_no_sender"
|
||||
sess = self.session_for_action(sender_id, inbound_raw, text=text)
|
||||
sess = self._detach_frozen_bookmark(sess, sender_id)
|
||||
btn = self.match_button(text, sess)
|
||||
if btn:
|
||||
return self.on_button(
|
||||
@@ -565,30 +768,30 @@ class AirTextInquiryFlow:
|
||||
tms_ready = assemble_air_query(facts)
|
||||
facts = dict(tms_ready["facts"])
|
||||
if not check["ok"] or not tms_ready["ok"]:
|
||||
# 询价必填或 TMS 查价参数不齐:回显已识别 + 待补充示例,不出询价确认
|
||||
keys = list(check["missing"] or [])
|
||||
for key in tms_ready.get("missing") or []:
|
||||
if key not in keys:
|
||||
keys.append(key)
|
||||
reply(
|
||||
copy.ask_clarify(
|
||||
new_sess = FlowSession(
|
||||
sender_id=sender_id,
|
||||
thread_id=sender_id,
|
||||
phase="clarify",
|
||||
business_line="AIR",
|
||||
facts=facts,
|
||||
immutable_text=sess.immutable_text if sess and sess.immutable_text else text,
|
||||
wait_version=(sess.wait_version + 1) if sess else 1,
|
||||
)
|
||||
return self._clarify_or_handoff(
|
||||
sess=sess,
|
||||
keys=keys,
|
||||
new_sess=new_sess,
|
||||
reply=reply,
|
||||
clarify_text=copy.ask_clarify(
|
||||
facts=facts,
|
||||
missing_keys=keys,
|
||||
transport_mode="空运",
|
||||
)
|
||||
),
|
||||
)
|
||||
self._save(
|
||||
FlowSession(
|
||||
sender_id=sender_id,
|
||||
thread_id=sender_id,
|
||||
phase="clarify",
|
||||
business_line="AIR",
|
||||
facts=facts,
|
||||
immutable_text=sess.immutable_text if sess and sess.immutable_text else text,
|
||||
wait_version=(sess.wait_version + 1) if sess else 1,
|
||||
)
|
||||
)
|
||||
return "clarify"
|
||||
|
||||
return self._create_and_quote(
|
||||
sender_id=sender_id,
|
||||
@@ -606,6 +809,11 @@ class AirTextInquiryFlow:
|
||||
reply: ReplyFn,
|
||||
) -> str:
|
||||
# 先落已确认字段,避免建单失败后补问轮把毛重/体积弄丢
|
||||
from agent.policy.handoff_human import void_virtual_no
|
||||
|
||||
old = self.session_of(sender_id)
|
||||
if old:
|
||||
void_virtual_no(old)
|
||||
self._save(
|
||||
FlowSession(
|
||||
sender_id=sender_id,
|
||||
|
||||
Reference in New Issue
Block a user