Files
mindfulness/server/app/tasks/reco.py
2026-02-11 13:50:02 +08:00

208 lines
5.8 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.
from __future__ import annotations
import asyncio
from datetime import datetime, timezone
from typing import Any, Optional
from celery import shared_task
from app.db.session import AsyncSessionLocal
from app.features.personalized_reco.content_repository.sqlalchemy_repo import SqlAlchemyContentRepository
from app.features.personalized_reco.content_repository.types import normalize_locale
from app.features.personalized_reco.reco_engine import recommend
from app.features.personalized_reco.reco_engine.types import RecoConstraints, RecoEngineResult, Scene
from app.features.user_profile_scoring.types import UserProfileV1_2
def _ensure_now(now: Optional[datetime]) -> datetime:
if now is None:
return datetime.now(timezone.utc)
if now.tzinfo is None:
return now.replace(tzinfo=timezone.utc)
return now
def _ensure_locale(locale: Optional[str]) -> str:
raw = (locale or "").strip() or "en"
# 严格校验只支持 en/tc允许 en-US 等在 normalize_locale 内归一化)
return str(normalize_locale(raw))
async def _run_reco_async(
*,
scene: Scene,
user_profile: UserProfileV1_2,
already_recommended_ids: list[Any],
touched_or_viewed_ids: list[Any],
k: int,
now: datetime,
locale: str,
) -> RecoEngineResult:
async with AsyncSessionLocal() as session:
repo = SqlAlchemyContentRepository(session)
return await recommend(
repo=repo,
scene=scene,
user_profile=user_profile,
already_recommended_ids=list(already_recommended_ids or []),
touched_or_viewed_ids=list(touched_or_viewed_ids or []),
k=int(k),
now=now,
locale=locale,
constraints=RecoConstraints(),
)
async def run_reco_payload_async(
*,
scene: Scene,
user_profile: UserProfileV1_2,
already_recommended_ids: Optional[list[Any]] = None,
touched_or_viewed_ids: Optional[list[Any]] = None,
k: Optional[int] = None,
now: Optional[datetime] = None,
locale: Optional[str] = None,
) -> dict[str, Any]:
"""
在“已有事件循环”内运行推荐并返回 payload。
用途:
- 供 Push 等 async 任务内部调用,避免 `asyncio.run()` 嵌套导致 RuntimeError
- 也便于未来在 API/任务间复用
"""
effective_now = _ensure_now(now)
effective_locale = _ensure_locale(locale)
# k 默认按场景(与 generate 保持一致)
if k is None:
k_i = 30 if scene == "feed" else 1
else:
k_i = int(k)
result = await _run_reco_async(
scene=scene,
user_profile=user_profile,
already_recommended_ids=list(already_recommended_ids or []),
touched_or_viewed_ids=list(touched_or_viewed_ids or []),
k=int(k_i),
now=effective_now,
locale=effective_locale,
)
return result.model_dump()
def _run_reco_sync(
*,
scene: Scene,
user_profile: UserProfileV1_2,
already_recommended_ids: list[Any],
touched_or_viewed_ids: list[Any],
k: int,
now: Optional[datetime],
locale: Optional[str],
) -> dict[str, Any]:
effective_now = _ensure_now(now)
effective_locale = _ensure_locale(locale)
result = asyncio.run(
_run_reco_async(
scene=scene,
user_profile=user_profile,
already_recommended_ids=already_recommended_ids,
touched_or_viewed_ids=touched_or_viewed_ids,
k=int(k),
now=effective_now,
locale=effective_locale,
)
)
# 默认不存结果但返回值可用于开发调试worker 通常 ignore_result
return result.model_dump()
@shared_task(name="tasks.reco.generate")
def generate(
*,
scene: Scene,
user_profile: dict[str, Any],
already_recommended_ids: Optional[list[Any]] = None,
touched_or_viewed_ids: Optional[list[Any]] = None,
k: Optional[int] = None,
now: Optional[str] = None,
locale: Optional[str] = None,
) -> dict[str, Any]:
"""
推荐生成任务(通用入口)。
说明:
- 入参尽量保持小(避免 Redis 队列膨胀)
- 默认 worker 配置为 ignore_result但这里仍返回结构便于本地调试
"""
# 解析 user_profile严格按 V1.2
u = UserProfileV1_2.model_validate(user_profile or {})
# k 默认按场景(与 API 一致)
if k is None:
k_i = 30 if scene == "feed" else 1
else:
k_i = int(k)
# now 支持 ISO 字符串
dt: Optional[datetime]
if not now:
dt = None
else:
raw = str(now).strip()
if raw.endswith("Z"):
raw = raw[:-1] + "+00:00"
try:
dt = datetime.fromisoformat(raw)
except Exception:
dt = None
return _run_reco_sync(
scene=scene,
user_profile=u,
already_recommended_ids=list(already_recommended_ids or []),
touched_or_viewed_ids=list(touched_or_viewed_ids or []),
k=k_i,
now=dt,
locale=locale,
)
def _deliver_push_placeholder(payload: dict[str, Any]) -> None:
"""
Push 下游写入占位函数V1 不接真实推送系统)。
"""
_ = payload
return None
@shared_task(name="tasks.reco.push_once")
def push_once(
*,
user_profile: dict[str, Any],
already_recommended_ids: Optional[list[Any]] = None,
touched_or_viewed_ids: Optional[list[Any]] = None,
now: Optional[str] = None,
locale: Optional[str] = None,
) -> dict[str, Any]:
"""
单次 Push 生成(占位任务)。
"""
payload = generate(
scene="push",
user_profile=user_profile,
already_recommended_ids=already_recommended_ids,
touched_or_viewed_ids=touched_or_viewed_ids,
k=1,
now=now,
locale=locale,
)
_deliver_push_placeholder(payload)
return payload