PMFlow/backend/app/services/signal_planner/signal_strategic_analysis_s...

106 lines
3.1 KiB
Python

"""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