落地私聊空运询价与TMS报价回填,按后台模板出单。
查到价后把币种、单位、数量、单价、备注写入报价单,避免销售只看到光秃秃的金额。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,9 +2,10 @@
|
||||
Handler 包:一类意图 / 一类卡片一个模块。
|
||||
|
||||
禁止:单文件处理全部卡片;禁止把 Schema 校验复制粘贴进每个 handler(放 schema/policy)。
|
||||
当前:echo 联调 + 其余意图空壳(待接 RouteDecision)。
|
||||
当前:文字询价空运闭环 + echo 联调残留。
|
||||
"""
|
||||
|
||||
from agent.handlers.echo_text import handle_echo_text
|
||||
from agent.handlers.text_inquiry import handle_text_inquiry, handle_text_other
|
||||
|
||||
__all__ = ["handle_echo_text"]
|
||||
__all__ = ["handle_echo_text", "handle_text_inquiry", "handle_text_other"]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""
|
||||
意图:模板卡 / 补问卡动作(骨架)。
|
||||
意图:询价卡片按钮(跳过协同 / 采用 / 不采用 / 成交)。
|
||||
|
||||
正式:校验 clarification token 绑定的 waitVersion + 允许动作集合。
|
||||
本文件职责:校验当前会话允许的动作后交给流程执行。
|
||||
禁止:不校验 wait/允许集合就改主账;禁止一个函数处理未声明的新按钮。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -9,6 +10,10 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from agent.channel.queue import get_message_store
|
||||
from agent.policy import inquiry_copy as copy
|
||||
from agent.policy.air_text_flow import get_air_text_flow
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -19,14 +24,48 @@ def handle_card_action(
|
||||
wait_version: int,
|
||||
action: str,
|
||||
payload: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""骨架:仅记日志;无 sender_id 直接返回。"""
|
||||
flow=None,
|
||||
store=None,
|
||||
) -> str:
|
||||
"""
|
||||
处理一次卡片动作。
|
||||
|
||||
wait_version:会话书签版本;错版本丢弃,不重放副作用。
|
||||
payload 须带 task_id / event_key,才能找回点卡当时的工单。
|
||||
"""
|
||||
_ = thread_id
|
||||
if not (sender_id or "").strip():
|
||||
return
|
||||
logger.info(
|
||||
"handler.card_action stub sender=%s thread=%s wait=%s action=%s",
|
||||
sender_id,
|
||||
thread_id,
|
||||
wait_version,
|
||||
action,
|
||||
return "reject_no_sender"
|
||||
engine = flow or get_air_text_flow()
|
||||
sess = engine.session_for_action(sender_id, payload)
|
||||
if sess and wait_version and sess.wait_version != wait_version:
|
||||
logger.info("card waitVersion 不匹配 expect=%s got=%s", sess.wait_version, wait_version)
|
||||
return "card_discard"
|
||||
|
||||
msg_store = store or get_message_store()
|
||||
|
||||
def reply(text: str, extra: dict | None = None) -> None:
|
||||
# 销售询价正文不加 [test];[test] 只给运维告警
|
||||
body = text
|
||||
extra = dict(extra or {})
|
||||
wo = ""
|
||||
if sess is not None:
|
||||
wo = (sess.work_order_no or "").strip()
|
||||
if not wo:
|
||||
wo = copy.work_order_from_card_meta(payload)
|
||||
code = str((payload or {}).get("response_code") or extra.get("response_code") or "")[:24]
|
||||
ok, info = msg_store.enqueue_outbound(
|
||||
touser=sender_id,
|
||||
content=body,
|
||||
dedupe_key=(
|
||||
f"card:{sender_id}:{wo}:{code}:"
|
||||
f"{sess.wait_version if sess else 0}:{action}:{body[:20]}"
|
||||
),
|
||||
payload=extra,
|
||||
)
|
||||
if not ok:
|
||||
logger.warning("card 出站被去重 sender=%s wo=%s info=%s", sender_id, wo or "-", info)
|
||||
|
||||
return engine.on_button(
|
||||
sender_id=sender_id, action=action, reply=reply, card_meta=payload
|
||||
)
|
||||
|
||||
@@ -1,24 +1,62 @@
|
||||
"""
|
||||
意图:继续本单 / Graph resume(骨架)。
|
||||
意图:继续历史工单。
|
||||
|
||||
正式:校验 thread_id + waitVersion 后 Worker 槽 resume;禁止在 HTTP 回调线程 invoke。
|
||||
本文件职责:把 DeepSeek A 认出的续办意图交给流程,按工单号回到当时停点。
|
||||
工单号优先用路由结论;模型漏填时再从原话机读 WO 号(不在这里判断意图)。
|
||||
禁止:在本文件抽询价字段;禁止回调线程跑完整 LLM;禁止直连 TMS。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from agent.channel.queue import get_message_store
|
||||
from agent.channel.wecom.models import InboundMessage
|
||||
from agent.policy import inquiry_copy as copy
|
||||
from agent.policy.air_text_flow import get_air_text_flow
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def handle_continue_thread(message: InboundMessage, *, thread_id: str = "") -> None:
|
||||
"""骨架:仅记日志。"""
|
||||
def handle_continue_thread(
|
||||
message: InboundMessage,
|
||||
*,
|
||||
thread_id: str = "",
|
||||
flow=None,
|
||||
store=None,
|
||||
) -> str:
|
||||
"""
|
||||
续办一张历史工单:回到该单当时节点的卡片/话术。
|
||||
|
||||
thread_id:RouteDecision 里的工单号。副作用:outbox 回复;读主账。
|
||||
"""
|
||||
if not message.has_sender():
|
||||
return
|
||||
logger.info(
|
||||
"handler.continue_thread stub sender=%s thread_id=%s",
|
||||
message.sender_id,
|
||||
thread_id or "-",
|
||||
logger.warning("continue_thread 拒绝:无 sender_id")
|
||||
return "reject_no_sender"
|
||||
msg_store = store or get_message_store()
|
||||
engine = flow or get_air_text_flow()
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra: dict | None = None) -> None:
|
||||
replies.append(text)
|
||||
ok, info = msg_store.enqueue_outbound(
|
||||
touser=message.sender_id,
|
||||
content=text,
|
||||
dedupe_key=f"cont:{message.message_id}:{len(replies)}",
|
||||
payload=dict(extra or {}),
|
||||
)
|
||||
logger.info("continue_thread 出站 ok=%s info=%s", ok, info)
|
||||
|
||||
wo = (thread_id or "").strip() or copy.extract_work_order_no(message.content or "")
|
||||
phase = engine.resume_historical(
|
||||
sender_id=message.sender_id,
|
||||
work_order_no=wo,
|
||||
reply=reply,
|
||||
)
|
||||
logger.info(
|
||||
"continue_thread phase=%s sender=%s wo=%s",
|
||||
phase,
|
||||
message.sender_id,
|
||||
wo or "-",
|
||||
)
|
||||
return phase
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
意图:普通文字询价(空运闭环)。
|
||||
|
||||
本文件职责:入站文字 → 流程编排;回复只写入 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.policy.air_text_flow import get_air_text_flow
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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 get_air_text_flow()
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra: dict | None = None) -> None:
|
||||
# 销售询价正文不加 [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])
|
||||
|
||||
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
|
||||
from agent.policy.air_text_flow import get_air_text_flow
|
||||
|
||||
if not message.has_sender():
|
||||
return "reject_no_sender"
|
||||
msg_store = store or get_message_store()
|
||||
engine = get_air_text_flow()
|
||||
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)
|
||||
|
||||
body = copy.FIRST_CHAT
|
||||
msg_store.enqueue_outbound(
|
||||
touser=message.sender_id,
|
||||
content=body,
|
||||
dedupe_key=f"other:{message.message_id}",
|
||||
)
|
||||
return "text_other"
|
||||
Reference in New Issue
Block a user