Files
inquiry_robot/inquiry-agent/agent/policy/event_exception.py
T

242 lines
7.4 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.
"""
事件异常纯规则:等待种类、超时编码、点名对象、卡片接收人。
本文件职责:给 Worker 巡检判定用,不发企微、不写主账。
禁止:在回调线程做 IO;把六态写进本文件;把系统异常混进来。
"""
from __future__ import annotations
import logging
from typing import Any
from agent.schema.land_options import canonicalize_land_class
from agent.schema.sea_options import canonicalize_sea_class
logger = logging.getLogger(__name__)
WAIT_AIR_QUOTE = "air_quote"
WAIT_PRODUCT_QUOTE = "product_quote"
WAIT_ADOPT = "adopt"
WAIT_DEAL = "deal"
WAIT_LOST_REASON = "lost_reason"
TYPE_AIR = "航线响应超时"
TYPE_PRODUCT = "产品响应超时"
TYPE_SALES = "销售响应超时"
CARD_AIR = "航线响应异常"
CARD_PRODUCT = "产品响应异常"
CARD_SALES = "销售响应异常"
_SALES_CODES = {
"AIR": "air_sales",
"SEA": "sea_sales",
"LAND": "land_sales",
}
_LAND_PRODUCT_CODES = {
"普件(常用线路/常规车型)": "land_common",
"普件(东南亚、中港常用线路/常规车型)": "land_hk_common",
"普件(东南亚、中亚、中欧涉及国内+国外段)": "land_hk_cross",
"普件(东南亚、中亚、中欧涉及境外段清派等特殊要求)": "land_hk_special",
"急件": "land_hk_express",
}
_SALES_WAITS = {WAIT_ADOPT, WAIT_DEAL, WAIT_LOST_REASON}
def timeout_code(*, wait_kind: str, business_line: str, transport_class: str) -> str:
"""
按等待种类 + 运输方式 + 询价分类,对上后台超时规则编码。
对不上返回空串,调用方视为本步不监测。副作用:无。
"""
line = (business_line or "").strip().upper()
kind = (wait_kind or "").strip()
klass = (transport_class or "").strip()
if kind in _SALES_WAITS:
return _SALES_CODES.get(line, "")
if kind == WAIT_AIR_QUOTE:
return "air_route"
if kind == WAIT_PRODUCT_QUOTE:
if line == "SEA":
canon = canonicalize_sea_class(klass) or klass
if canon == "Port to Port":
return "sea_product_ptp"
if canon == "其他":
return "sea_product_other"
return ""
if line == "LAND":
canon = canonicalize_land_class(klass) or klass
return _LAND_PRODUCT_CODES.get(canon, "")
return ""
return ""
def exception_type(wait_kind: str) -> str:
"""工单「事件异常」字段用的类型名。"""
kind = (wait_kind or "").strip()
if kind == WAIT_AIR_QUOTE:
return TYPE_AIR
if kind == WAIT_PRODUCT_QUOTE:
return TYPE_PRODUCT
if kind in _SALES_WAITS:
return TYPE_SALES
return ""
def card_title(wait_kind: str) -> str:
"""询价消息服务卡片标题。"""
kind = (wait_kind or "").strip()
if kind == WAIT_AIR_QUOTE:
return CARD_AIR
if kind == WAIT_PRODUCT_QUOTE:
return CARD_PRODUCT
if kind in _SALES_WAITS:
return CARD_SALES
return ""
def format_timeout_hours(hours: float) -> str:
"""话术里的小时数:整数去小数点,0.25 保持原样。"""
try:
n = float(hours)
except (TypeError, ValueError):
return "0"
if n == int(n):
return str(int(n))
text = f"{n:.4f}".rstrip("0").rstrip(".")
return text or "0"
def append_summary(existing: str, type_name: str) -> str:
"""多次异常用分号拼接,同一类型不重复。"""
add = (type_name or "").strip()
cur = (existing or "").strip()
if not add:
return cur
if not cur or cur == "-":
return add
parts = [p.strip() for p in cur.split(";") if p.strip()]
if add in parts:
return cur
parts.append(add)
return ";".join(parts)
def start_wait(ledger: Any, *, work_order_no: str, wait_kind: str) -> None:
"""开始盯等待。失败只打日志,不抛给回调。"""
no = (work_order_no or "").strip()
kind = (wait_kind or "").strip()
if not no or not kind or ledger is None:
return
fn = getattr(ledger, "start_event_wait", None)
if not callable(fn):
return
try:
fn(work_order_no=no, wait_kind=kind)
except Exception:
logger.exception("start_event_wait 失败 wo=%s kind=%s", no, kind)
def clear_wait(ledger: Any, *, work_order_no: str) -> None:
"""清空当前等待,不改事件异常摘要。失败只打日志。"""
no = (work_order_no or "").strip()
if not no or ledger is None:
return
fn = getattr(ledger, "clear_event_wait", None)
if not callable(fn):
return
try:
fn(work_order_no=no)
except Exception:
logger.exception("clear_event_wait 失败 wo=%s", no)
def pick_mentions(
*,
wait_kind: str,
sales: dict[str, Any] | None,
members: list[dict[str, Any]] | None,
business_line: str,
) -> list[dict[str, Any]]:
"""
本单这次应回而未回、需要被点名的人。
航线只盯群里航线岗;海运/陆运产品盯对应产品岗;销售三类只盯本单销售。
不要询价机器人、不要群里其他人。
"""
kind = (wait_kind or "").strip()
line = (business_line or "").strip().upper()
if kind in _SALES_WAITS:
person = dict(sales or {})
uid = str(person.get("wecomId") or person.get("wecom_id") or "").strip()
if not uid:
return []
person.setdefault("wecomId", uid)
person.setdefault("name", str(person.get("name") or "").strip() or "销售")
return [person]
want = ""
if kind == WAIT_AIR_QUOTE:
want = "air"
elif kind == WAIT_PRODUCT_QUOTE:
if line == "SEA":
want = "sea"
elif line == "LAND":
want = "land"
if not want:
return []
out: list[dict[str, Any]] = []
seen: set[str] = set()
for raw in members or []:
if not isinstance(raw, dict):
continue
role = str(raw.get("roleCode") or raw.get("role_code") or "").strip().lower()
if role != want:
continue
uid = str(raw.get("wecomId") or raw.get("wecom_id") or "").strip()
if not uid or uid in seen:
continue
seen.add(uid)
row = dict(raw)
row.setdefault("wecomId", uid)
row.setdefault("name", str(raw.get("name") or "").strip() or uid)
out.append(row)
return out
def pick_app_targets(people: list[dict[str, Any]] | None) -> list[dict[str, Any]]:
"""勾了「接收事件异常消息提醒」且绑了企微的人。"""
out: list[dict[str, Any]] = []
seen: set[str] = set()
for raw in people or []:
if not isinstance(raw, dict):
continue
flag = raw.get("notifyEventException")
if flag not in (1, True, "1"):
continue
uid = str(raw.get("wecomId") or raw.get("wecom_id") or "").strip()
if not uid or uid in seen:
continue
seen.add(uid)
out.append(raw)
return out
def conversation_of(*, wait_kind: str, collab_chat_id: str) -> str:
"""
告警发在哪个会话:group 或 private。
采用/不采用永远私聊;航线/产品永远群;成交跟进和未成交原因有协同群才群。
"""
kind = (wait_kind or "").strip()
chat = (collab_chat_id or "").strip()
if kind == WAIT_ADOPT:
return "private"
if kind in {WAIT_AIR_QUOTE, WAIT_PRODUCT_QUOTE}:
return "group"
if kind in {WAIT_DEAL, WAIT_LOST_REASON}:
return "group" if chat else "private"
return "private"