125 lines
4.3 KiB
Python
125 lines
4.3 KiB
Python
"""Utility helpers for signal planner service."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from .shared import Any, Optional, Signal, json
|
|
|
|
|
|
class SignalPlannerUtilsMixin:
|
|
"""Type-safe conversion and fallback builder helpers."""
|
|
|
|
@staticmethod
|
|
def _as_dict(value: Any) -> dict[str, Any]:
|
|
if isinstance(value, dict):
|
|
return dict(value)
|
|
if isinstance(value, str):
|
|
try:
|
|
parsed = json.loads(value)
|
|
return parsed if isinstance(parsed, dict) else {}
|
|
except Exception:
|
|
return {}
|
|
return {}
|
|
|
|
@staticmethod
|
|
def _safe_int(value: Any) -> Optional[int]:
|
|
try:
|
|
return int(str(value).strip())
|
|
except (TypeError, ValueError, AttributeError):
|
|
return None
|
|
|
|
@staticmethod
|
|
def _safe_float(value: Any) -> Optional[float]:
|
|
try:
|
|
return float(value)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
@classmethod
|
|
def _resolve_outbox_targets(
|
|
cls,
|
|
*,
|
|
signal: Signal,
|
|
source_message_id: Optional[str],
|
|
source_chat_id: Optional[str],
|
|
) -> tuple[Optional[str], Optional[str]]:
|
|
strategic = cls._as_dict(signal.strategic_analysis)
|
|
source_context = (
|
|
strategic.get("source_context")
|
|
if isinstance(strategic.get("source_context"), dict)
|
|
else {}
|
|
)
|
|
resolved_chat_id = (
|
|
str(source_chat_id or "").strip()
|
|
or str(source_context.get("source_chat_id") or "").strip()
|
|
or str(source_context.get("chat_id") or "").strip()
|
|
or str(source_context.get("channel_ref") or "").strip()
|
|
or str(source_context.get("group_chat_id") or "").strip()
|
|
or None
|
|
)
|
|
resolved_source_message_id = (
|
|
str(source_message_id or "").strip()
|
|
or str(source_context.get("source_message_id") or "").strip()
|
|
or str(source_context.get("message_id") or "").strip()
|
|
or None
|
|
)
|
|
return resolved_chat_id, resolved_source_message_id
|
|
|
|
@classmethod
|
|
def _pick_fallback_assignee_user_id(
|
|
cls,
|
|
*,
|
|
members: list[dict[str, Any]],
|
|
) -> Optional[int]:
|
|
candidates: list[tuple[float, int]] = []
|
|
for item in members:
|
|
uid = cls._safe_int(item.get("user_id"))
|
|
workload_ratio = cls._safe_float(item.get("workload_ratio"))
|
|
if uid is None:
|
|
continue
|
|
candidates.append((float(workload_ratio if workload_ratio is not None else 1000.0), int(uid)))
|
|
if not candidates:
|
|
return None
|
|
candidates.sort(key=lambda row: (row[0], row[1]))
|
|
return int(candidates[0][1])
|
|
|
|
@classmethod
|
|
def _build_fallback_planner_result(
|
|
cls,
|
|
*,
|
|
signal: Signal,
|
|
members: list[dict[str, Any]],
|
|
reason: str,
|
|
) -> dict[str, Any]:
|
|
fallback_assignee = cls._pick_fallback_assignee_user_id(members=members)
|
|
summary_seed = (
|
|
str(signal.summary or "").strip()
|
|
or str(signal.title or "").strip()
|
|
or str(signal.content or "").strip()
|
|
or "信号已入库,模型结构化输出异常,已自动降级生成初始执行建议。"
|
|
)
|
|
execution_plan: list[dict[str, Any]] = [
|
|
{
|
|
"step": "澄清信号目标与边界",
|
|
"owner_user_id": fallback_assignee,
|
|
"eta": "4h",
|
|
},
|
|
{
|
|
"step": "拆解最小可执行任务并确认优先级",
|
|
"owner_user_id": fallback_assignee,
|
|
"eta": "1d",
|
|
},
|
|
]
|
|
return {
|
|
"summary": summary_seed[:180],
|
|
"priority": str(signal.priority or "medium").strip() or "medium",
|
|
"execution_plan": execution_plan,
|
|
"risks": [
|
|
"模型返回非预期结构,已使用降级策略生成建议,请人工复核。",
|
|
],
|
|
"confidence": 0.2,
|
|
"assignee_user_id": fallback_assignee,
|
|
"assignee_reason": "按当前最低负载成员自动指派(降级模式)",
|
|
"fallback_reason": str(reason or "structured_schema_invalid")[:200],
|
|
"fallback_mode": True,
|
|
}
|