Eliot0110's picture
fix: missing lib
f4c999b
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"
)