Преглед изворни кода

feat: Python 侧新增 portrait_generation_graph AI 生成/重画用户画像 LangGraph

E2E Test Bot пре 3 дана
родитељ
комит
0f904ba311
1 измењених фајлова са 141 додато и 0 уклоњено
  1. 141 0
      cfc-langgraph/app/graphs/portrait_generation_graph.py

+ 141 - 0
cfc-langgraph/app/graphs/portrait_generation_graph.py

@@ -0,0 +1,141 @@
+"""用户画像生成/重画 LangGraph
+
+输入(由 Java 传入):
+- user_id: 用户ID
+- member_id: 成员ID(可空)
+- previous_portrait: 旧画像文本(重画时传入,首次生成为 None)
+- profile_snapshot: 五维画像快照 dict(含 member/dimension_scores/body_metrics/mind_metrics/problem_domains)
+- behavior_summary: 行为摘要 dict
+
+输出:
+- portrait_text: 生成的画像文本(中文,自然语言描述)
+"""
+from typing import TypedDict
+from langgraph.graph import StateGraph, START, END
+from langchain_openai import ChatOpenAI
+from langchain_core.messages import SystemMessage, HumanMessage
+from app.config import settings
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class PortraitGenerationState(TypedDict):
+    user_id: int
+    member_id: int | None
+    previous_portrait: str | None
+    profile_snapshot: dict | None
+    behavior_summary: dict | None
+    portrait_text: str | None
+
+
+SYSTEM_PROMPT = """你是「浠艾福」家庭成长平台的 AI 画像分析师。
+
+任务:根据用户的行为数据、五维能量指标、健康画像,生成一段自然语言的「用户画像」文本。
+
+画像用途:作为后续所有 AI 对话(健康教练、管家、营养师、通用聊天)的 SystemMessage 注入,帮助 AI 更懂用户,给出更个性化的建议。
+
+生成要求:
+1. 语言自然、温暖、具体,像一位了解家庭的成长顾问写的观察笔记
+2. 必须包含:核心特质、行为模式、关注点、潜在需求
+3. 长度:200-500 字,不要过长
+4. 不要列举原始数据(如"身 7.2 智 6.5"),要转化为洞察(如"身体维度相对突出,但智力维度有提升空间")
+5. 如有 previous_portrait,重画时要体现"变化"与"延续":哪些特质稳定,哪些有新变化,原因可能是什么
+
+输出格式:纯文本,无 JSON,无标记。"""
+
+
+def create_portrait_generation_graph():
+    llm = ChatOpenAI(
+        model=settings.llm_model,
+        api_key=settings.llm_api_key,
+        base_url=settings.llm_base_url,
+        temperature=0.7,
+    )
+
+    builder = StateGraph(PortraitGenerationState)
+
+    async def generate_portrait(state: PortraitGenerationState) -> dict:
+        previous = state.get("previous_portrait")
+        profile = state.get("profile_snapshot") or {}
+        behavior = state.get("behavior_summary") or {}
+
+        context_parts = []
+
+        if profile:
+            member = profile.get("member") or {}
+            name = member.get("name", "用户")
+            age = member.get("age")
+            gender = member.get("gender")
+            context_parts.append(f"用户:{name},{age}岁,{gender}")
+
+            dims = profile.get("dimension_scores") or {}
+            if dims:
+                context_parts.append(
+                    f"五维评分:身 {dims.get('body','?')} 智 {dims.get('wisdom','?')} "
+                    f"心 {dims.get('mind','?')} 行 {dims.get('action','?')} 富 {dims.get('wealth','?')}"
+                )
+
+            body = profile.get("body_metrics") or {}
+            if body.get("sleep_dur_avg"):
+                context_parts.append(f"平均睡眠:{body['sleep_dur_avg']}h/天")
+            if body.get("exercise_count_week"):
+                context_parts.append(f"周运动:{body['exercise_count_week']}次")
+
+            mind = profile.get("mind_metrics") or {}
+            if mind.get("stress_avg"):
+                context_parts.append(f"平均压力:{mind['stress_avg']}/10")
+
+            problems = profile.get("problem_domains") or []
+            if problems:
+                context_parts.append(f"关注问题:{', '.join(problems[:5])}")
+
+        if behavior:
+            context_parts.append("行为摘要:" + str(behavior))
+
+        context_text = "\n".join(context_parts) or "暂无额外数据"
+
+        if previous:
+            human_content = f"""【重画任务】请基于以下最新数据,重新生成用户画像。
+
+【原画像(参考)】:
+{previous}
+
+【最新数据】:
+{context_text}
+
+要求:保留原画像中仍有效的核心特质,更新已变化的部分,体现"变化与延续"。"""
+        else:
+            human_content = f"""【生成任务】请基于以下数据,生成用户画像。
+
+【数据】:
+{context_text}"""
+
+        messages = [
+            SystemMessage(content=SYSTEM_PROMPT),
+            HumanMessage(content=human_content),
+        ]
+
+        response = await llm.ainvoke(messages)
+        portrait_text = response.content.strip()
+
+        logger.info("生成画像完成: user_id=%d, member_id=%s, len=%d",
+                    state["user_id"], state.get("member_id"), len(portrait_text))
+
+        return {"portrait_text": portrait_text}
+
+    builder.add_node("generate_portrait", generate_portrait)
+    builder.add_edge(START, "generate_portrait")
+    builder.add_edge("generate_portrait", END)
+
+    return builder.compile()
+
+
+_portrait_graph = None
+
+
+def get_portrait_graph():
+    global _portrait_graph
+    if _portrait_graph is None:
+        _portrait_graph = create_portrait_generation_graph()
+    return _portrait_graph