adapter.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. from fastapi import APIRouter, HTTPException
  2. from pydantic import BaseModel, ConfigDict
  3. from typing import Optional
  4. from app.graphs.chat_graph import create_chat_graph
  5. from app.graphs.analysis_graph import create_analysis_graph
  6. from app.graphs.health_coach_graph import create_health_coach_graph
  7. import uuid
  8. import time
  9. router = APIRouter(prefix="/api/v1", tags=["adapter"])
  10. # ---- Dify-compatible request/response models ----
  11. class DifyMessage(BaseModel):
  12. role: str
  13. content: str
  14. class DifyChatRequest(BaseModel):
  15. # 前端 userId/conversationId 可能为数字,允许 int/float 自动转 str,避免 422
  16. model_config = ConfigDict(coerce_numbers_to_str=True)
  17. query: str = ""
  18. user: str = "anonymous"
  19. conversation_id: str = ""
  20. messages: list[DifyMessage] = []
  21. inputs: dict = {}
  22. response_mode: str = "blocking" # blocking / streaming
  23. user_id: str = ""
  24. bot_name: str = "AI管家"
  25. class DifyAnalysisRequest(BaseModel):
  26. # 前端 userId 可能为数字,允许 int/float 自动转 str,避免 422
  27. model_config = ConfigDict(coerce_numbers_to_str=True)
  28. report_id: Optional[int] = None
  29. user_id: Optional[str] = None
  30. focus: Optional[str] = None
  31. messages: list[DifyMessage] = []
  32. inputs: dict = {}
  33. class DifyChoice(BaseModel):
  34. index: int
  35. message: dict
  36. finish_reason: str = "stop"
  37. class DifyUsage(BaseModel):
  38. prompt_tokens: int = 0
  39. completion_tokens: int = 0
  40. total_tokens: int = 0
  41. class DifyResponse(BaseModel):
  42. id: str
  43. object: str = "chat.completion"
  44. created: int
  45. model: str = "langgraph-cfc"
  46. choices: list[DifyChoice]
  47. usage: DifyUsage = DifyUsage()
  48. metadata: dict = {}
  49. # ---- Helpers ----
  50. def _now_ts() -> int:
  51. return int(time.time())
  52. def _extract_query(req: DifyChatRequest) -> str:
  53. if req.query:
  54. return req.query
  55. # fallback: take last user message
  56. for msg in reversed(req.messages):
  57. if msg.role == "user":
  58. return msg.content
  59. return ""
  60. def _to_langgraph_context(req: DifyChatRequest) -> dict:
  61. ctx = {}
  62. if isinstance(req.inputs, dict):
  63. ctx["child_id"] = req.inputs.get("child_id")
  64. ctx["report_id"] = req.inputs.get("report_id")
  65. ctx["family_id"] = req.inputs.get("family_id")
  66. return ctx
  67. # ---- Dify-compatible endpoints ----
  68. @router.post("/chat/completion", response_model=DifyResponse)
  69. async def chat_completion(req: DifyChatRequest):
  70. query = _extract_query(req)
  71. if not query:
  72. raise HTTPException(status_code=400, detail="query 为空")
  73. graph = create_chat_graph()
  74. initial_state = {
  75. "query": query,
  76. "user_id": int(req.user_id) if str(req.user_id).isdigit() else 0,
  77. "conversation_id": req.conversation_id or None,
  78. "intent": None,
  79. "context": _to_langgraph_context(req),
  80. "messages": None,
  81. "answer": None,
  82. "tasks": [],
  83. "sources": [],
  84. }
  85. config = {
  86. "configurable": {"thread_id": req.conversation_id or str(req.user_id or "default")},
  87. }
  88. result = await graph.ainvoke(initial_state, config)
  89. answer = result.get("answer") or ""
  90. return DifyResponse(
  91. id=f"chatcmpl-{uuid.uuid4().hex[:24]}",
  92. created=_now_ts(),
  93. choices=[
  94. DifyChoice(
  95. index=0,
  96. message={"role": "assistant", "content": answer},
  97. finish_reason="stop",
  98. )
  99. ],
  100. usage=DifyUsage(
  101. prompt_tokens=len(query.split()),
  102. completion_tokens=len(answer.split()),
  103. total_tokens=len(query.split()) + len(answer.split()),
  104. ),
  105. )
  106. @router.post("/analysis/run", response_model=DifyResponse)
  107. async def analysis_run(req: DifyAnalysisRequest):
  108. graph = create_analysis_graph()
  109. report_id = req.report_id
  110. if report_id is None and isinstance(req.inputs, dict):
  111. report_id = req.inputs.get("report_id")
  112. initial_state = {
  113. "report_id": report_id,
  114. "user_id": int(req.user_id) if str(req.user_id).isdigit() else 0,
  115. "focus": req.focus,
  116. "report_data": None,
  117. "survey_data": None,
  118. "dimension_scores": None,
  119. "analysis": None,
  120. "recommendations": [],
  121. }
  122. result = await graph.ainvoke(initial_state, {})
  123. analysis = result.get("analysis") or ""
  124. recommendations = result.get("recommendations") or []
  125. content = analysis
  126. if recommendations:
  127. content += "\n\n建议:\n" + "\n".join(f"- {r}" for r in recommendations)
  128. return DifyResponse(
  129. id=f"analysis-{uuid.uuid4().hex[:24]}",
  130. created=_now_ts(),
  131. choices=[
  132. DifyChoice(
  133. index=0,
  134. message={"role": "assistant", "content": content},
  135. finish_reason="stop",
  136. )
  137. ],
  138. usage=DifyUsage(
  139. prompt_tokens=len((req.focus or "").split()),
  140. completion_tokens=len(content.split()),
  141. total_tokens=len((req.focus or "").split()) + len(content.split()),
  142. ),
  143. )
  144. @router.post("/health/coach", response_model=DifyResponse)
  145. async def health_coach(req: DifyChatRequest):
  146. query = _extract_query(req)
  147. if not query:
  148. raise HTTPException(status_code=400, detail="query 为空")
  149. graph = create_health_coach_graph()
  150. initial_state = {
  151. "query": query,
  152. "user_id": int(req.user_id) if str(req.user_id).isdigit() else 0,
  153. "child_id": int(req.inputs.get("child_id")) if req.inputs.get("child_id") else None,
  154. "conversation_id": req.conversation_id or None,
  155. "context": _to_langgraph_context(req),
  156. "answer": None,
  157. "sources": [],
  158. "memory_messages": None,
  159. }
  160. config = {
  161. "configurable": {"thread_id": req.conversation_id or f"health_{req.user_id}"},
  162. }
  163. result = await graph.ainvoke(initial_state, config)
  164. answer = result.get("answer") or ""
  165. sources = result.get("sources") or []
  166. metadata = {}
  167. if sources:
  168. metadata["sources"] = [
  169. {"title": s.get("title", ""), "type": s.get("type", "knowledge")}
  170. for s in sources
  171. ]
  172. return DifyResponse(
  173. id=f"health-{uuid.uuid4().hex[:24]}",
  174. created=_now_ts(),
  175. model="langgraph-health-coach",
  176. choices=[
  177. DifyChoice(
  178. index=0,
  179. message={"role": "assistant", "content": answer},
  180. finish_reason="stop",
  181. )
  182. ],
  183. usage=DifyUsage(
  184. prompt_tokens=len(query.split()),
  185. completion_tokens=len(answer.split()),
  186. total_tokens=len(query.split()) + len(answer.split()),
  187. ),
  188. metadata=metadata,
  189. )