questionnaire.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import json
  2. from typing import TypedDict, Optional
  3. from langgraph.graph import StateGraph, START, END
  4. from langchain_core.messages import HumanMessage
  5. from ..llm.client import get_llm
  6. from ..prompts.questionnaire import PARENT_TEMPLATE, CHILD_TEMPLATE
  7. from ..schemas.questionnaire import Questionnaire, GenerateRequest
  8. class GraphState(TypedDict):
  9. request: GenerateRequest
  10. _prompt: Optional[str]
  11. raw_response: str
  12. questionnaire: Optional[dict]
  13. error: Optional[str]
  14. def _build_context(req: GenerateRequest) -> str:
  15. """格式化家庭背景信息字符串"""
  16. if not req.family_context:
  17. return "信息暂缺,请基于基本关系作答"
  18. parts = []
  19. members = req.family_context.get("members", [])
  20. if members:
  21. parts.append(f"家庭共有 {len(members)} 位成员")
  22. cohabitation = req.family_context.get("cohabitation")
  23. if cohabitation:
  24. parts.append(f"是否同住:{'是' if cohabitation else '否'}")
  25. conflicts = req.family_context.get("recent_conflicts", [])
  26. if conflicts:
  27. parts.append(f"近期有 {len(conflicts)} 次主要冲突")
  28. return ";".join(parts) if parts else "信息暂缺,请基于基本关系作答"
  29. def build_prompt(state: GraphState) -> GraphState:
  30. req = state["request"]
  31. # 优先使用 Java 后端传入的 prompt 模板(可配置),缺省回退内置模板
  32. template = req.prompt_template or (
  33. PARENT_TEMPLATE if req.relationship_type == "parent" else CHILD_TEMPLATE
  34. )
  35. # 构建双方画像数据
  36. resp_prof = req.respondent_profile or {}
  37. mem_prof = req.member_profile or {}
  38. def safe_get(d, key, default=""):
  39. return str(d.get(key)) if d and key in d and d.get(key) else default
  40. respondent_name = safe_get(resp_prof, "nickname", "家长")
  41. member_name = safe_get(mem_prof, "nickname", req.member_name)
  42. respondent_age = safe_get(resp_prof, "age", "未知")
  43. member_age = safe_get(mem_prof, "age", "未知")
  44. respondent_gender = safe_get(resp_prof, "gender", "未知")
  45. member_gender = safe_get(mem_prof, "gender", "未知")
  46. respondent_zodiac = safe_get(resp_prof, "zodiac", "未知")
  47. member_zodiac = safe_get(mem_prof, "zodiac", "未知")
  48. relationship_desc = "家长与孩子" if req.relationship_type == "parent" else "孩子与家长"
  49. family_ctx = _build_context(req)
  50. prompt = template.format(
  51. member_name=member_name,
  52. respondent_name=respondent_name,
  53. relationship_desc=relationship_desc,
  54. respondent_age=respondent_age,
  55. respondent_gender=respondent_gender,
  56. respondent_zodiac=respondent_zodiac,
  57. member_age=member_age,
  58. member_gender=member_gender,
  59. member_zodiac=member_zodiac,
  60. family_context=family_ctx,
  61. )
  62. return {**state, "_prompt": prompt}
  63. def call_llm(state: GraphState) -> GraphState:
  64. llm = get_llm()
  65. messages = [HumanMessage(content=state["_prompt"])]
  66. response = llm.invoke(messages)
  67. return {**state, "raw_response": response.content}
  68. def validate(state: GraphState) -> GraphState:
  69. raw = state["raw_response"]
  70. # 提取 JSON 块
  71. text = raw.strip()
  72. if "```json" in text:
  73. text = text.split("```json")[1].split("```")[0].strip()
  74. elif "```" in text:
  75. text = text.split("```")[1].split("```")[0].strip()
  76. try:
  77. data = json.loads(text)
  78. q = Questionnaire(**data)
  79. if not q.validate_structure():
  80. return {**state, "error": "问卷结构校验失败:至少需要1题且每题有options或scale"}
  81. return {**state, "questionnaire": data, "error": None}
  82. except Exception as e:
  83. return {**state, "error": f"JSON 解析失败: {str(e)}"}
  84. def build_questionnaire_graph():
  85. graph = StateGraph(GraphState)
  86. graph.add_node("build_prompt", build_prompt)
  87. graph.add_node("call_llm", call_llm)
  88. graph.add_node("validate", validate)
  89. graph.add_edge(START, "build_prompt")
  90. graph.add_edge("build_prompt", "call_llm")
  91. graph.add_edge("call_llm", "validate")
  92. graph.add_edge("validate", END)
  93. return graph.compile()
  94. _questionnaire_graph = None
  95. def get_questionnaire_graph():
  96. global _questionnaire_graph
  97. if _questionnaire_graph is None:
  98. _questionnaire_graph = build_questionnaire_graph()
  99. return _questionnaire_graph