106 lines
3.1 KiB
Python
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
|