""" 意图路由:DeepSeek A → 唯一 RouteDecision。 禁止:在本包产生业务副作用(建单、发消息、改六态)。 正式路径:DeepSeekARouter(strict tool);route_stub 仅兼容旧自检名。 """ from __future__ import annotations import logging from dataclasses import dataclass from typing import Optional from agent.llm import LlmGateway, LlmMode from agent.routing.decision import RouteDecision from agent.routing.deepseek_a import DeepSeekARouter from agent.routing.dispatch import HANDLER_REGISTRY, dispatch_inbound, resolve_handler from agent.routing.ordered_actions import ( ORDERED_ACTIONS_TOOL_NAME, OrderedActionsError, OrderedActionsResult, ordered_actions_tool_definition, parse_ordered_actions_arguments, ) # IntentRouter 定义见下 logger = logging.getLogger(__name__) @dataclass class IntentRouter: """ 路由门面。 - route():优先 DeepSeek A(默认 stub,不开费) - enqueue_ordered_actions():快回路径入 LLM Stream """ llm: Optional[LlmGateway] = None deepseek_a: Optional[DeepSeekARouter] = None allow_network: bool = False def _a(self) -> DeepSeekARouter: if self.deepseek_a is not None: return self.deepseek_a return DeepSeekARouter.from_settings(allow_network=self.allow_network) def route_stub(self, *, text: str, sender_id: str) -> RouteDecision: """ 兼容旧名:等价于无网络的 A stub fixture。 正式生产请用 route() 并在 Worker 开 allow_network + 数据确认。 """ return self._a().route_stub_fixture(text=text, sender_id=sender_id) def route(self, *, text: str, sender_id: str) -> RouteDecision: """一条入站一个 RouteDecision。""" return self._a().route(text=text, sender_id=sender_id) def route_inbound( self, *, text: str, sender_id: str, event: str = "", ) -> RouteDecision: """ 入站后再路由。 点卡片是结构化事件,不进大模型。 自由文字交给 DeepSeek A 认意图(续办 / 新询价 / 闲聊),不要求固定口令。 """ if not sender_id: return RouteDecision(intent="reject_no_sender", confidence=1.0) kind = (event or "").strip().lower() if kind in {"template_card_event", "click", "templatecardevent"}: return RouteDecision(intent="card_action", confidence=1.0) from agent.handlers.text_inquiry import is_land_confirm_action from agent.channel.wecom.models import InboundMessage from agent.schema.land_options import looks_like_confirm if looks_like_confirm(text or ""): if is_land_confirm_action( InboundMessage( sender_id=sender_id, content=text or "", message_id="route-land-confirm", ) ): logger.info("route 陆运核对卡确定:跳过闲聊模型 sender=%s", sender_id) return RouteDecision(intent="ordinary_text_inquiry", confidence=1.0) logger.info("route 确定类短句但无陆运核对卡,仍走模型 sender=%s", sender_id) return self.route(text=text, sender_id=sender_id) def enqueue_ordered_actions( self, *, text: str, sender_id: str, idempotency_key: str, ) -> str: """ 将 A 路由任务入 LLM 队列(快回路径用)。 返回 stream entry id;无 LlmGateway 时抛错。 """ if self.llm is None: raise RuntimeError("IntentRouter 未注入 LlmGateway") result = self.llm.enqueue( mode=LlmMode.ORDERED_ACTIONS, payload={"text": text, "sender_id": sender_id}, idempotency_key=idempotency_key, ) if not result.accepted: raise RuntimeError(result.reason or "enqueue_rejected") return result.entry_id __all__ = [ "RouteDecision", "IntentRouter", "DeepSeekARouter", "ORDERED_ACTIONS_TOOL_NAME", "OrderedActionsError", "OrderedActionsResult", "ordered_actions_tool_definition", "parse_ordered_actions_arguments", "HANDLER_REGISTRY", "dispatch_inbound", "resolve_handler", ]