Files
inquiry_robot/inquiry-agent/agent/routing/deepseek_a.py
T

160 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
DeepSeek A 调用壳:组装 messages + route_decision tool,解析为 RouteDecision。
本文件职责:Worker 槽内跑 A 路由;默认不打外网(需 LLM_DATA_USAGE_CONFIRMED)。
禁止:在企微回调线程同步调用本模块的网络路径。
"""
from __future__ import annotations
import hashlib
import logging
from dataclasses import dataclass
from typing import Any, Optional
from agent.config import Settings, get_settings
from agent.llm.http_client import LlmHttpClient, LlmHttpResult
from agent.llm.modes import LlmMode
from agent.routing.decision import RouteDecision
from agent.routing.ordered_actions import (
ORDERED_ACTIONS_TOOL_NAME,
OrderedActionsError,
extract_tool_arguments_from_chat_raw,
ordered_actions_tool_definition,
parse_ordered_actions_arguments,
)
logger = logging.getLogger(__name__)
_SYSTEM = (
"You are DeepSeek-A for inquiry routing. "
"You must call the function tool route_decision exactly once. "
"Do not write user-facing text. Do not extract business fields."
)
def _digest_text(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
@dataclass
class DeepSeekARouter:
"""
DeepSeek A 路由器。
allow_network:是否允许真 HTTP;仍受 settings.llm_data_usage_confirmed 约束。
"""
settings: Settings
http: LlmHttpClient
allow_network: bool = False
@classmethod
def from_settings(
cls,
settings: Optional[Settings] = None,
*,
allow_network: bool = False,
) -> "DeepSeekARouter":
cfg = settings or get_settings()
return cls(
settings=cfg,
http=LlmHttpClient.from_settings(cfg),
allow_network=allow_network,
)
def build_messages(self, *, text: str, sender_id: str) -> list[dict[str, Any]]:
"""构造仅含 inbound_facts 的 messages(不做字段抽取)。"""
facts = {
"sender_id": sender_id,
"text": text,
"text_sha256": _digest_text(text or ""),
}
return [
{"role": "system", "content": _SYSTEM},
{
"role": "user",
"content": (
"inbound_facts (JSON-like):\n"
f"sender_id={facts['sender_id']}\n"
f"text_sha256={facts['text_sha256']}\n"
f"text={facts['text']}\n"
"Call route_decision once."
),
},
]
def route_from_tool_args(self, args: dict[str, Any] | str) -> RouteDecision:
"""仅解析已有 tool 参数(单测 / stub / 回放)。"""
return parse_ordered_actions_arguments(args).to_route_decision()
def route_stub_fixture(self, *, text: str, sender_id: str) -> RouteDecision:
"""
无外网固定夹具:把非空文本标成 ordinary_text_inquiry。
仅骨架联调;正式生产必须走模型或已落库的 tool 回放。
"""
if not sender_id:
return RouteDecision(intent="reject_no_sender", confidence=1.0)
content = (text or "").strip()
if not content:
return RouteDecision(intent="ignore_empty", confidence=1.0)
digest = _digest_text(content)
args = {
"decision_id": f"STUB-{digest[:16]}",
"intent": "ordinary_text_inquiry",
"ordered_action": {
"action_type": "ORDINARY_TEXT_INQUIRY",
"order": 1,
"target": {"work_order_no": ""},
"evidence": {
"source_ref": f"inbound:{sender_id}",
"content_digest": digest,
},
},
}
logger.info("DeepSeekA stub fixture sender=%s", sender_id)
return self.route_from_tool_args(args)
def route(self, *, text: str, sender_id: str) -> RouteDecision:
"""
执行 A 路由。
无 sender_id → reject;未开网络或未确认数据用途 → stub fixture;
开网络 → tool_choice 强制 route_decision。
"""
if not sender_id:
return RouteDecision(intent="reject_no_sender", confidence=1.0)
if not self.allow_network or not self.settings.llm_data_usage_confirmed:
if self.allow_network and not self.settings.llm_data_usage_confirmed:
logger.warning(
"LLM_DATA_USAGE_CONFIRMED 未确认,拒绝打真实 DeepSeek A,改用 stub"
)
return self.route_stub_fixture(text=text, sender_id=sender_id)
messages = self.build_messages(text=text, sender_id=sender_id)
tools = [ordered_actions_tool_definition()]
result: LlmHttpResult = self.http.chat(
mode=LlmMode.ORDERED_ACTIONS,
messages=messages,
tools=tools,
tool_choice={"type": "function", "function": {"name": ORDERED_ACTIONS_TOOL_NAME}},
)
if not result.ok:
logger.warning("DeepSeek A HTTP 失败 err=%s", result.error)
return RouteDecision(
intent="route_model_failed",
confidence=0.0,
raw={"error": result.error, "provider": result.provider},
)
try:
args = extract_tool_arguments_from_chat_raw(result.raw)
return self.route_from_tool_args(args)
except OrderedActionsError as exc:
logger.warning("DeepSeek A 解析失败: %s", exc)
return RouteDecision(
intent="route_parse_failed",
confidence=0.0,
raw={"error": str(exc)},
)