Files
inquiry_robot/inquiry-agent/agent/routing/dispatch.py
T
jillion886andCursor 99da1faf30 落地私聊空运询价与TMS报价回填,按后台模板出单。
查到价后把币种、单位、数量、单价、备注写入报价单,避免销售只看到光秃秃的金额。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 20:25:35 +08:00

160 lines
5.2 KiB
Python

"""
入站分发壳:RouteDecision.intent → 唯一 handler。
本文件职责:维护意图→可调用对象表;dispatch_inbound 只查表调用。
禁止:在本文件写补问/建单/TMS 业务;禁止一个函数处理全部卡片。
"""
from __future__ import annotations
import logging
from typing import Callable, Optional
from agent.channel.wecom.models import InboundMessage
from agent.handlers import abnormal, card_action, close_deal, continue_thread, new_inquiry
from agent.handlers.echo_text import handle_echo_text
from agent.handlers.text_inquiry import handle_text_inquiry, handle_text_other
from agent.routing.decision import RouteDecision
logger = logging.getLogger(__name__)
# Handler 签名约定:只收 InboundMessage(出站/store 由 handler 自行取依赖)
HandlerFn = Callable[[InboundMessage], None]
def _noop(message: InboundMessage) -> None:
"""忽略类意图:不做事。"""
logger.info("dispatch.noop sender=%s msg=%s", message.sender_id, message.message_id)
def _wrap_echo(message: InboundMessage) -> None:
"""联调残留:仅 echo_text 意图使用。"""
from agent.channel.queue import get_message_store
from agent.config import get_settings
settings = get_settings()
handle_echo_text(
message,
get_message_store(),
ytd_env=settings.ytd_env,
reply_template=settings.wecom_callback_reply_text,
)
def _wrap_text_inquiry(message: InboundMessage) -> None:
"""普通文字询价:空运闭环。"""
handle_text_inquiry(message)
def _wrap_text_other(message: InboundMessage) -> None:
"""闲聊:回首次引导;若正等按钮则当按钮。"""
handle_text_other(message)
def _wrap_new(message: InboundMessage) -> None:
new_inquiry.handle_new_inquiry(message)
def _wrap_continue(message: InboundMessage, *, thread_id: str = "") -> None:
continue_thread.handle_continue_thread(message, thread_id=thread_id)
def _wrap_card(message: InboundMessage) -> None:
"""
卡片意图:把企微 TaskId / EventKey 原样交给流程,按当时工单号找回会话。
action 用正文(EventKey 或按钮文案),禁止写死 unknown。
"""
from agent.policy import inquiry_copy as copy
action = copy.canonical_button(message.content or "") or (message.content or "").strip()
card_action.handle_card_action(
sender_id=message.sender_id,
thread_id="",
wait_version=0,
action=action or "unknown",
payload=dict(message.raw or {}),
)
def _wrap_close(message: InboundMessage) -> None:
close_deal.handle_close_deal(
sender_id=message.sender_id,
inquiry_no="",
outcome="unknown",
)
def _wrap_abnormal(message: InboundMessage) -> None:
abnormal.handle_abnormal(sender_id=message.sender_id, inquiry_no="", reason="dispatch")
def _wrap_route_failed(message: InboundMessage) -> None:
"""模型没给出路由结论:请销售再说一次,不假装听懂。"""
from agent.channel.queue import get_message_store
from agent.policy import inquiry_copy as copy
get_message_store().enqueue_outbound(
touser=message.sender_id,
content=copy.ROUTE_MODEL_FAILED,
dedupe_key=f"route-fail:{message.message_id}",
)
# 意图 → handler(空壳或联调 echo);扩展时只改表,不改 ChannelRuntime 分支森林
HANDLER_REGISTRY: dict[str, HandlerFn] = {
"echo_text": _wrap_echo,
"ordinary_text_inquiry": _wrap_text_inquiry,
"ordinary_text_other": _wrap_text_other,
"attachment_inquiry": _wrap_new,
"image_inquiry": _wrap_new,
"multi_segment_transport": _wrap_new,
"tms_read_only_quote": _wrap_continue,
"new_inquiry": _wrap_new,
"continue_thread": _wrap_continue,
"card_action": _wrap_card,
"close_deal": _wrap_close,
"abnormal": _wrap_abnormal,
"reject_no_sender": _noop,
"ignore_empty": _noop,
"stub_needs_model": _wrap_echo,
"route_model_failed": _wrap_route_failed,
"route_parse_failed": _wrap_route_failed,
}
def resolve_handler(intent: str) -> Optional[HandlerFn]:
"""查表;未知意图返回 None。"""
return HANDLER_REGISTRY.get(intent)
def dispatch_inbound(message: InboundMessage, decision: RouteDecision) -> str:
"""
按 RouteDecision 调用唯一 handler。
返回:实际调用的 intent 名,或 unknown_intent / skipped。
副作用:仅 handler 内产生;本函数不做业务。
"""
if not message.has_sender() and decision.intent != "reject_no_sender":
logger.warning("dispatch 拒绝:无 sender_id")
HANDLER_REGISTRY["reject_no_sender"](message)
return "reject_no_sender"
fn = resolve_handler(decision.intent)
if fn is None:
logger.warning("dispatch 未知意图 intent=%s → noop", decision.intent)
_noop(message)
return "unknown_intent"
logger.info(
"dispatch intent=%s sender=%s msg=%s wo=%s",
decision.intent,
message.sender_id,
message.message_id,
decision.thread_id or "-",
)
if decision.intent == "continue_thread":
continue_thread.handle_continue_thread(message, thread_id=decision.thread_id)
return decision.intent
fn(message)
return decision.intent