adapter.py 6.3 KB

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