from fastapi import FastAPI, HTTPException, UploadFile, File from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from utils.validators import ChatRequest, ChatResponse from utils.logger import log from modules.travel_assistant import TravelAssistant import traceback import tempfile import shutil # --- FastAPI 应用设置 --- app = FastAPI( title="Modular Travel Assistant API", description="一个采用模块化和分层架构的旅行助手API", version="2.0.0", docs_url="/docs", redoc_url="/redoc" ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # --- 全局服务实例 --- try: log.info("🔄 开始初始化 Travel Assistant 服务...") assistant = TravelAssistant() SERVICE_READY = True log.info("🚀 FastAPI 应用启动成功,服务已就绪。") except Exception as e: assistant = None SERVICE_READY = False log.critical(f"💥 FATAL: 服务初始化失败: {e}") log.critical(f"💥 错误详情: {traceback.format_exc()}") # --- API 端点 --- @app.get("/") async def root(): """根路径 - 提供基本信息""" ai_available = False if SERVICE_READY and assistant and hasattr(assistant, 'ai_model'): ai_available = assistant.ai_model.is_available() return { "message": "Travel Assistant API", "version": "2.0.0", "status": "ready" if SERVICE_READY else "degraded", "ai_model_status": "available" if ai_available else "unavailable", "docs": "/docs", "health": "/health" } @app.get("/health") async def health_check(): """健康检查端点""" if SERVICE_READY and assistant: ai_available = assistant.ai_model.is_available() if hasattr(assistant.ai_model, 'is_available') else False return { "status": "healthy" if ai_available else "degraded", "service_ready": True, "ai_model_available": ai_available, "components": { "config": True, "knowledge_base": len(assistant.kb.knowledge) > 0 if hasattr(assistant, 'kb') else False, "ai_model": ai_available, "session_manager": True } } else: return JSONResponse( status_code=503, content={ "status": "unhealthy", "service_ready": False, "ai_model_available": False } ) @app.post("/api/chat", response_model=ChatResponse) async def chat_endpoint(request: ChatRequest): """主要的聊天API端点""" if not SERVICE_READY or not assistant: raise HTTPException( status_code=503, detail="Service Unavailable: Backend assistant failed to initialize." ) try: log.info(f"收到聊天请求: {request.message[:50]}...") if request.persona_key: log.info(f"用户选择的persona: {request.persona_key}") reply, session_id, status_info, history = assistant.chat( request.message, request.session_id, request.history or [], request.persona_key # 传递persona参数 ) log.info(f"聊天响应生成成功,会话ID: {session_id}") return ChatResponse( reply=reply, session_id=session_id, status_info=status_info, history=history ) except Exception as e: log.error(f"❌ Chat endpoint error: {e}", exc_info=True) raise HTTPException( status_code=500, detail="Internal Server Error: Failed to process chat request." ) except Exception as e: log.error(f"❌ Chat endpoint error: {e}", exc_info=True) raise HTTPException( status_code=500, detail="Internal Server Error: Failed to process chat request." ) @app.post("/api/reset") async def reset_session(session_id: str): """重置会话端点""" if not SERVICE_READY or not assistant: raise HTTPException( status_code=503, detail="Service Unavailable" ) try: assistant.session_manager.reset(session_id) log.info(f"会话重置成功: {session_id}") return { "message": "Session reset successfully", "session_id": session_id, "status": "success" } except Exception as e: log.error(f"❌ Reset session error: {e}") raise HTTPException( status_code=500, detail="Failed to reset session" ) @app.post("/api/transcribe-audio") async def transcribe_audio_endpoint(file: UploadFile = File(...)): """ 接收音频文件,进行语音转文本,并返回转录结果。 """ # 1. 检查核心服务是否就绪 if not SERVICE_READY or not assistant: raise HTTPException( status_code=503, detail="Service Unavailable: Backend assistant failed to initialize." ) # 2. 检查上传的是否是音频文件 (基础检查) if not file.content_type.startswith("audio/"): raise HTTPException( status_code=400, detail=f"Invalid file type: {file.content_type}. Please upload an audio file." ) tmp_path = None try: # 3. 创建一个带正确后缀的临时文件,以便模型能识别 # 例如,如果上传 a.wav, 临时文件也会是 xxx.wav suffix = os.path.splitext(file.filename)[1] with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: # 将上传的文件内容写入临时文件 shutil.copyfileobj(file.file, tmp) tmp_path = tmp.name log.info(f"音频文件已临时保存到: {tmp_path}") # 4. 调用 TravelAssistant 中的转录方法 transcribed_text = assistant.transcribe_audio(audio_path=tmp_path) log.info(f"音频转录成功,内容: {transcribed_text[:50]}...") # 5. 将转录文本作为 JSON 返回 return {"transcription": transcribed_text} except Exception as e: log.error(f"❌ Audio transcription error: {e}", exc_info=True) raise HTTPException( status_code=500, detail="Internal Server Error: Failed to transcribe audio." ) finally: # 6. 无论成功与否,都确保清理临时文件 if tmp_path and os.path.exists(tmp_path): os.remove(tmp_path) log.info(f"临时文件已清理: {tmp_path}") @app.get("/api/debug/sessions") async def debug_sessions(): """调试:查看所有会话状态""" if not SERVICE_READY or not assistant: raise HTTPException(status_code=503, detail="Service not ready") try: # 先清理旧会话 cleaned = assistant.session_manager.cleanup_old_sessions() sessions_summary = assistant.session_manager.get_all_sessions_summary() sessions_summary["cleaned_sessions"] = cleaned return sessions_summary except Exception as e: log.error(f"❌ Debug sessions error: {e}") raise HTTPException(status_code=500, detail="Failed to get sessions") @app.get("/api/debug/session/{session_id}") async def debug_single_session(session_id: str): """调试:查看单个会话详情""" if not SERVICE_READY or not assistant: raise HTTPException(status_code=503, detail="Service not ready") if session_id in assistant.session_manager.sessions: session_data = assistant.session_manager.sessions[session_id] metadata = assistant.session_manager.session_metadata.get(session_id, {}) return { "session_data": session_data, "metadata": metadata, "summary": assistant.session_manager._get_session_summary(session_id) } else: raise HTTPException(status_code=404, detail="Session not found") @app.post("/api/debug/cleanup") async def cleanup_sessions(max_age_hours: int = 24): """手动清理旧会话""" if not SERVICE_READY or not assistant: raise HTTPException(status_code=503, detail="Service not ready") try: cleaned_count = assistant.session_manager.cleanup_old_sessions(max_age_hours) return { "message": f"Cleaned {cleaned_count} old sessions", "cleaned_sessions": cleaned_count, "remaining_sessions": len(assistant.session_manager.sessions) } except Exception as e: log.error(f"❌ Cleanup error: {e}") raise HTTPException(status_code=500, detail="Failed to cleanup sessions") # --- 全局异常处理 --- @app.exception_handler(Exception) async def global_exception_handler(request, exc): """全局异常处理器""" log.error(f"❌ 未处理的异常: {exc}", exc_info=True) return JSONResponse( status_code=500, content={"detail": "Internal server error", "message": "请稍后重试"} ) # HuggingFace Spaces 和本地运行配置 if __name__ == "__main__": import uvicorn log.info("🔧 本地开发模式启动...") uvicorn.run( app, host="0.0.0.0", port=7860, reload=True, log_level="info" )