"""Signal Planner 战略分析写回服务。""" from __future__ import annotations import json from typing import Any, Optional from sqlalchemy import and_, select from sqlalchemy.ext.asyncio import AsyncSession from ...models import Signal from ...utils.timezone import now_cn_naive class SignalStrategicAnalysisService: """收口 Planner 写回逻辑,避免调用链散落裸写。""" @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 @classmethod async def merge_and_save( cls, db: AsyncSession, *, signal_id: int, planner_result: dict[str, Any], tenant_id: int, ) -> Signal | None: row = ( await db.execute( select(Signal).where( and_( Signal.tenant_id == int(tenant_id), Signal.id == int(signal_id), ) ).limit(1) ) ).scalar_one_or_none() if row is None: return None strategic = cls._as_dict(row.strategic_analysis) strategic["ai_planner"] = planner_result if isinstance(planner_result, dict) else {} strategic["ai_planner_meta"] = { "updated_at": now_cn_naive().isoformat(), "function_name": "signal_planner_analyze", } row.strategic_analysis = json.dumps(strategic, ensure_ascii=False) row.planner_status = "completed" row.planner_analyzed_at = now_cn_naive() assignee_user_id = cls._safe_int((planner_result or {}).get("assignee_user_id")) row.planner_assignee_user_id = assignee_user_id await db.flush() return row @classmethod async def mark_failed( cls, db: AsyncSession, *, signal_id: int, tenant_id: int, reason: str, ) -> Signal | None: row = ( await db.execute( select(Signal).where( and_( Signal.tenant_id == int(tenant_id), Signal.id == int(signal_id), ) ).limit(1) ) ).scalar_one_or_none() if row is None: return None strategic = cls._as_dict(row.strategic_analysis) strategic["ai_planner_error"] = { "reason": str(reason or "planner_failed")[:1000], "failed_at": now_cn_naive().isoformat(), } row.strategic_analysis = json.dumps(strategic, ensure_ascii=False) row.planner_status = "failed" row.planner_analyzed_at = now_cn_naive() await db.flush() return row