from fastapi import APIRouter, HTTPException from pydantic import BaseModel, ConfigDict from typing import Optional from app.graphs.chat_graph import create_chat_graph from app.graphs.analysis_graph import create_analysis_graph from app.graphs.health_coach_graph import create_health_coach_graph import uuid import time router = APIRouter(prefix="/api/v1", tags=["adapter"]) # ---- Dify-compatible request/response models ---- class DifyMessage(BaseModel): role: str content: str class DifyChatRequest(BaseModel): # 前端 userId/conversationId 可能为数字,允许 int/float 自动转 str,避免 422 model_config = ConfigDict(coerce_numbers_to_str=True) query: str = "" user: str = "anonymous" conversation_id: str = "" messages: list[DifyMessage] = [] inputs: dict = {} response_mode: str = "blocking" # blocking / streaming user_id: str = "" bot_name: str = "AI管家" class DifyAnalysisRequest(BaseModel): # 前端 userId 可能为数字,允许 int/float 自动转 str,避免 422 model_config = ConfigDict(coerce_numbers_to_str=True) report_id: Optional[int] = None user_id: Optional[str] = None focus: Optional[str] = None messages: list[DifyMessage] = [] inputs: dict = {} class DifyChoice(BaseModel): index: int message: dict finish_reason: str = "stop" class DifyUsage(BaseModel): prompt_tokens: int = 0 completion_tokens: int = 0 total_tokens: int = 0 class DifyResponse(BaseModel): id: str object: str = "chat.completion" created: int model: str = "langgraph-cfc" choices: list[DifyChoice] usage: DifyUsage = DifyUsage() metadata: dict = {} # ---- Helpers ---- def _now_ts() -> int: return int(time.time()) def _extract_query(req: DifyChatRequest) -> str: if req.query: return req.query # fallback: take last user message for msg in reversed(req.messages): if msg.role == "user": return msg.content return "" def _to_langgraph_context(req: DifyChatRequest) -> dict: ctx = {} if isinstance(req.inputs, dict): ctx["child_id"] = req.inputs.get("child_id") ctx["report_id"] = req.inputs.get("report_id") ctx["family_id"] = req.inputs.get("family_id") return ctx # ---- Dify-compatible endpoints ---- @router.post("/chat/completion", response_model=DifyResponse) async def chat_completion(req: DifyChatRequest): query = _extract_query(req) if not query: raise HTTPException(status_code=400, detail="query 为空") graph = create_chat_graph() initial_state = { "query": query, "user_id": int(req.user_id) if str(req.user_id).isdigit() else 0, "conversation_id": req.conversation_id or None, "intent": None, "context": _to_langgraph_context(req), "messages": None, "answer": None, "tasks": [], "sources": [], } config = { "configurable": {"thread_id": req.conversation_id or str(req.user_id or "default")}, } result = await graph.ainvoke(initial_state, config) answer = result.get("answer") or "" return DifyResponse( id=f"chatcmpl-{uuid.uuid4().hex[:24]}", created=_now_ts(), choices=[ DifyChoice( index=0, message={"role": "assistant", "content": answer}, finish_reason="stop", ) ], usage=DifyUsage( prompt_tokens=len(query.split()), completion_tokens=len(answer.split()), total_tokens=len(query.split()) + len(answer.split()), ), ) @router.post("/analysis/run", response_model=DifyResponse) async def analysis_run(req: DifyAnalysisRequest): graph = create_analysis_graph() report_id = req.report_id if report_id is None and isinstance(req.inputs, dict): report_id = req.inputs.get("report_id") initial_state = { "report_id": report_id, "user_id": int(req.user_id) if str(req.user_id).isdigit() else 0, "focus": req.focus, "report_data": None, "survey_data": None, "dimension_scores": None, "analysis": None, "recommendations": [], } result = await graph.ainvoke(initial_state, {}) analysis = result.get("analysis") or "" recommendations = result.get("recommendations") or [] content = analysis if recommendations: content += "\n\n建议:\n" + "\n".join(f"- {r}" for r in recommendations) return DifyResponse( id=f"analysis-{uuid.uuid4().hex[:24]}", created=_now_ts(), choices=[ DifyChoice( index=0, message={"role": "assistant", "content": content}, finish_reason="stop", ) ], usage=DifyUsage( prompt_tokens=len((req.focus or "").split()), completion_tokens=len(content.split()), total_tokens=len((req.focus or "").split()) + len(content.split()), ), ) @router.post("/health/coach", response_model=DifyResponse) async def health_coach(req: DifyChatRequest): query = _extract_query(req) if not query: raise HTTPException(status_code=400, detail="query 为空") graph = create_health_coach_graph() initial_state = { "query": query, "user_id": int(req.user_id) if str(req.user_id).isdigit() else 0, "child_id": int(req.inputs.get("child_id")) if req.inputs.get("child_id") else None, "conversation_id": req.conversation_id or None, "context": _to_langgraph_context(req), "answer": None, "sources": [], "memory_messages": None, } config = { "configurable": {"thread_id": req.conversation_id or f"health_{req.user_id}"}, } result = await graph.ainvoke(initial_state, config) answer = result.get("answer") or "" sources = result.get("sources") or [] metadata = {} if sources: metadata["sources"] = [ {"title": s.get("title", ""), "type": s.get("type", "knowledge")} for s in sources ] return DifyResponse( id=f"health-{uuid.uuid4().hex[:24]}", created=_now_ts(), model="langgraph-health-coach", choices=[ DifyChoice( index=0, message={"role": "assistant", "content": answer}, finish_reason="stop", ) ], usage=DifyUsage( prompt_tokens=len(query.split()), completion_tokens=len(answer.split()), total_tokens=len(query.split()) + len(answer.split()), ), metadata=metadata, )