portrait_service.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. """用户画像 prompt 组装服务
  2. 职责:
  3. 1. 拉取用户自定义画像文本(600s 缓存)
  4. 2. 拉取 profile_snapshot 并渲染为文本(900s 缓存)
  5. 3. 拼装成 SystemMessage 内容;无数据时返回 None
  6. """
  7. import time
  8. import logging
  9. from app.tools.java_client import JavaClient
  10. logger = logging.getLogger(__name__)
  11. _TTL_PROMPT = 600 # 用户自定义 prompt 缓存 10 分钟
  12. _TTL_SNAPSHOT = 900 # 画像快照缓存 15 分钟
  13. _cache_prompt: dict = {}
  14. _cache_snapshot: dict = {}
  15. def _render_snapshot(profile: dict) -> str:
  16. """将 profile Map 渲染为中文指标清单文本"""
  17. if not profile or "error" in profile:
  18. return ""
  19. lines = []
  20. member = profile.get("member") or {}
  21. name = member.get("name", "用户")
  22. age = member.get("age", "?")
  23. gender_raw = str(member.get("gender", ""))
  24. gender = "男" if gender_raw == "male" else "女" if gender_raw == "female" else "未知"
  25. lines.append(f"## 画像对象:{name}({age}岁,{gender})")
  26. dims = profile.get("dimension_scores") or {}
  27. if dims:
  28. lines.append(f"五维评分:身 {dims.get('body', '?')} 智 {dims.get('wisdom', '?')} "
  29. f"心 {dims.get('mind', '?')} 行 {dims.get('action', '?')} 富 {dims.get('wealth', '?')}")
  30. body = profile.get("body_metrics") or {}
  31. if body.get("sleep_dur_avg"):
  32. lines.append(f"平均睡眠:{body['sleep_dur_avg']}小时/天")
  33. if body.get("exercise_count_week"):
  34. lines.append(f"周运动频次:{body['exercise_count_week']}次")
  35. mind = profile.get("mind_metrics") or {}
  36. if mind.get("stress_avg"):
  37. lines.append(f"平均压力:{mind['stress_avg']}/10")
  38. problems = profile.get("problem_domains") or []
  39. if problems:
  40. lines.append(f"关注问题域:{', '.join(problems[:5])}")
  41. return "\n".join(lines)
  42. async def get_user_portrait_text(java: JavaClient, user_id: int, member_id) -> str | None:
  43. """返回画像 SystemMessage 内容;无画像数据时返回 None"""
  44. parts = []
  45. # 1. 用户自定义 prompt(600s 缓存)
  46. now = time.time()
  47. cached_text, cached_ts = _cache_prompt.get(user_id, (None, 0))
  48. if now - cached_ts < _TTL_PROMPT and cached_text is not None:
  49. user_text = cached_text
  50. else:
  51. try:
  52. client = await java._get_client()
  53. resp = await client.post("/api/user/portrait/get", json={"userId": user_id}, timeout=5.0)
  54. data = resp.json()
  55. user_text = (data.get("data") or {}).get("portraitPrompt") or ""
  56. _cache_prompt[user_id] = (user_text, now)
  57. except Exception as e:
  58. logger.warning("拉取用户画像 prompt 失败: %s", e)
  59. user_text = ""
  60. if user_text.strip():
  61. parts.append(user_text)
  62. # 2. 快照渲染文本(仅当有 member_id 时)
  63. if member_id:
  64. snap_now = time.time()
  65. cached_snap, cached_snap_ts = _cache_snapshot.get(int(member_id), (None, 0))
  66. if snap_now - cached_snap_ts < _TTL_SNAPSHOT and cached_snap is not None:
  67. snap_text = cached_snap
  68. else:
  69. try:
  70. profile = await java.get_member_profile(int(member_id))
  71. snap_text = _render_snapshot(profile) if profile else ""
  72. _cache_snapshot[int(member_id)] = (snap_text, snap_now)
  73. except Exception as e:
  74. logger.warning("拉取成员画像快照失败: %s", e)
  75. snap_text = ""
  76. if snap_text.strip():
  77. if user_text.strip():
  78. parts.append("\n---参考指标---\n" + snap_text)
  79. else:
  80. parts.append("以下是用户画像数据(系统自动生成),请结合这些数据给出更针对性的建议:\n" + snap_text)
  81. return "\n\n".join(parts) if parts else None
  82. def clear_cache(user_id: int | None = None, member_id: int | None = None):
  83. """清除缓存"""
  84. if user_id is not None:
  85. _cache_prompt.pop(user_id, None)
  86. if member_id is not None:
  87. _cache_snapshot.pop(member_id, None)
  88. if user_id is None and member_id is None:
  89. _cache_prompt.clear()
  90. _cache_snapshot.clear()