from __future__ import annotations import logging from typing import TYPE_CHECKING, Any, Optional, Tuple from core.post_ops import negotiation_strategy_reply logger = logging.getLogger("cs_agent") if TYPE_CHECKING: from core.pydantic_ai_agent import AgentDeps, ConversationState, CustomerMessage, CustomerServiceAgent def _select_agent_by_intent( agent: "CustomerServiceAgent", message: "CustomerMessage", state: "ConversationState", ) -> Tuple[Optional[Any], str]: """ AI 意图优先路由;识别不到时返回 (None, "intent:none"),由关键词兜底。 """ try: from utils.intent_analyzer import detect_intent decision = detect_intent(message.msg or "") intent = (decision.intent or "").strip() source = decision.source or "none" score = float(decision.score or 0.0) except Exception: intent, source, score = "", "error", 0.0 if not intent: return None, "intent:none" if intent in ("询价", "砍价"): return agent.agent_pricing, f"intent:{intent}|src:{source}|score:{score:.3f}" if intent in ("修改", "加急"): return agent.agent_processing, f"intent:{intent}|src:{source}|score:{score:.3f}" if intent == "售后": return agent.agent_after_sale, f"intent:{intent}|src:{source}|score:{score:.3f}" if intent == "转接": return agent.agent_after_sale, f"intent:{intent}|src:{source}|score:{score:.3f}" if intent in ("打招呼", "批量", "发图"): target = agent.agent_after_sale if state.stage == "售后" else agent.agent return target, f"intent:{intent}|src:{source}|score:{score:.3f}" return None, f"intent:unmapped:{intent}|src:{source}|score:{score:.3f}" def select_target_agent(agent: "CustomerServiceAgent", message: "CustomerMessage", state: "ConversationState") -> Tuple[Any, str]: msg_lower = message.msg.lower() pricing_kw = ["多少钱", "多少一张", "报价", "给个价", "几块", "价位", "能便宜点吗"] processing_kw = ["安排", "处理一下", "开始做", "做一下", "尽快", "加急", "付款了", "已付款"] similar_kw = ["有一样的", "有一样吗", "一样的吗", "类似的", "类似的吗", "同款", "相似", "类似吗"] order_markers = ["[系统订单信息]", "订单状态", "买家已付款"] risk_kw = [ "黄色", "擦边", "色情", "涉黄", "涉政", "政治", "裸", "不雅", "天安门", "政治人物", "政治事件", "领导人", "党政", "习近平", "毛泽东", "邓小平", "江泽民", "胡锦涛", "特朗普", "拜登", "普京", "泽连斯基", "地图", "地形图", "行政区划图", "卫星地图", ] target_agent = agent.agent_after_sale if state.stage == "售后" else agent.agent ai_target, ai_reason = _select_agent_by_intent(agent, message, state) if ai_target is not None: return ai_target, ai_reason risk_hit = any(k in msg_lower for k in risk_kw) or agent._is_political_inquiry(message.msg) or agent._is_map_inquiry(message.msg) if risk_hit: return agent.agent_risk, "keyword:risk" if any(k in message.msg for k in order_markers): return agent.agent_order, "keyword:order" if any(k in msg_lower for k in processing_kw): return agent.agent_processing, "keyword:processing" if any(k in msg_lower for k in pricing_kw): return agent.agent_pricing, "keyword:pricing" if any(k in msg_lower for k in similar_kw): return agent.agent_similar, "keyword:similar" return target_agent, "fallback:default" async def execute_ai_turn( agent: "CustomerServiceAgent", *, message: "CustomerMessage", state: "ConversationState", user_prompt: str, deps: "AgentDeps", history: list, ) -> str: target_agent, route_reason = select_target_agent(agent, message, state) logger.info("[路由] %s", route_reason) result = await target_agent.run(user_prompt, deps=deps, message_history=history) agent.message_histories[message.from_id] = result.all_messages()[-30:] reply_text = agent._colloquialize_reply(agent._normalize_reply_text(result.output)) strategy_reply = negotiation_strategy_reply(message.msg, state) if strategy_reply: reply_text = strategy_reply try: from config.config import MIN_PRICE_FLOOR import re offer = None m = re.search(r"(\d{1,4})\s*(?:元|块|块钱|元钱)\b", message.msg) if m: offer = int(m.group(1)) else: m2 = re.search(r"(?:能|可以|可否|能否)\s*(\d{1,4})\b", message.msg) offer = int(m2.group(1)) if m2 else None st = agent._get_conversation_state(message.from_id) floor = st.last_min_price if isinstance(st.last_min_price, int) and st.last_min_price > 0 else MIN_PRICE_FLOOR if offer is not None and offer < floor: reply_text = "不好意思" except Exception: pass try: from config.config import MIN_PRICE_FLOOR import re st = agent._get_conversation_state(message.from_id) floor = st.last_min_price if isinstance(st.last_min_price, int) and st.last_min_price > 0 else MIN_PRICE_FLOOR def _adjust(text: str) -> str: def _repl(m: Any): num = int(m.group(1)) adj = max(floor, round(num / 5) * 5) return m.group(0).replace(str(num), str(adj)) patterns = [ r"按(\d{1,4})元", r"报价[::]\s*(\d{1,4})\s*元", r"(\d{1,4})\s*元一张", r"打包(\d{1,4})\s*元", ] t = text for p in patterns: t = re.sub(p, _repl, t) return t reply_text = _adjust(reply_text or "") except Exception: pass for msg in result.new_messages(): for part in getattr(msg, "parts", []): part_type = type(part).__name__ if "ToolCall" in part_type: logger.info( "[THINK/TOOL_CALL] %s(%s)", getattr(part, "tool_name", ""), getattr(part, "args", ""), ) elif "ToolReturn" in part_type: ret = str(getattr(part, "content", ""))[:120] logger.info("[THINK/TOOL_RETURN] %s", ret) logger.info("[THINK/RAW_OUTPUT] %r", reply_text) return reply_text