Spaces:
Sleeping
Sleeping
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 端点 --- | |
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" | |
} | |
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 | |
} | |
) | |
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." | |
) | |
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" | |
) | |
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}") | |
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") | |
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") | |
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") | |
# --- 全局异常处理 --- | |
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" | |
) |