Files
course-agent-od/backend/main.py
T
pengxiao 4bb50ae9c1 feat: multimodal RAG with PDF image extraction and display
Extract images from PDFs using pymupdf, generate descriptions via
Qwen3-VL-8B, store in ChromaDB alongside text chunks, and render
images in chat answers. Includes image proxy rewrite, force re-process
endpoint, and VLM API timeout.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:14:40 +08:00

332 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
国土空间规划课程智能体后端服务
FastAPI应用入口
"""
import os
import sys
from pathlib import Path
from datetime import datetime
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
import uvicorn
# 设置系统时区为北京时间
os.environ['TZ'] = 'Asia/Shanghai'
# 添加src目录到Python路径
sys.path.append(str(Path(__file__).parent / "src"))
from src.core.config import get_settings, ensure_directories, is_postgresql_database
from src.core.database import create_tables, check_database_connection, get_database_info
from src.api import auth, chat
# 获取配置
settings = get_settings()
# 确保必要目录存在
ensure_directories()
# 创建FastAPI应用
app = FastAPI(
title=settings.app_name,
version=settings.app_version,
description="基于大模型的国土空间规划课程智能体系统",
docs_url="/docs",
redoc_url="/redoc"
)
# 配置CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 启动知识库文件监控服务
def startup_knowledge_base():
"""启动知识库服务"""
try:
from src.services.file_watcher_service import start_file_watcher
start_file_watcher()
print("知识库文件监控服务启动成功")
except Exception as e:
print(f"启动知识库文件监控服务失败: {str(e)}")
def seed_forum_categories():
"""初始化论坛默认分类"""
from src.core.database import get_db
from src.models.forum import ForumCategory
DEFAULT_CATEGORIES = [
{"slug": "announcements", "name": "课程公告", "description": "课程通知、作业安排与重要信息"},
{"slug": "course-learning", "name": "学习讨论", "description": "交流学习心得,讨论课程内容与难点"},
{"slug": "system-feedback", "name": "使用反馈", "description": "分享系统使用经验,提出改进建议"},
]
db = next(get_db())
try:
existing_count = db.query(ForumCategory).count()
if existing_count > 0:
print(f"论坛分类已存在({existing_count} 个),跳过初始化")
return
for cat_data in DEFAULT_CATEGORIES:
category = ForumCategory(**cat_data)
db.add(category)
db.commit()
print(f"已创建 {len(DEFAULT_CATEGORIES)} 个论坛分类")
except Exception as e:
db.rollback()
print(f"初始化论坛分类失败: {e}")
finally:
db.close()
# 应用启动事件
@app.on_event("startup")
async def startup_event():
"""应用启动时执行"""
import time
import asyncio
import sys
print("=" * 50, file=sys.stderr)
print("应用启动事件开始", file=sys.stderr)
print("=" * 50, file=sys.stderr)
sys.stderr.flush()
# 等待数据库可用(重试机制,处理 DNS 解析延迟)
max_retries = 10
retry_delay = 2
for attempt in range(max_retries):
try:
print(f"尝试连接数据库(尝试 {attempt + 1}/{max_retries}...", file=sys.stderr)
sys.stderr.flush()
# 尝试创建数据库表
create_tables()
print("数据库表创建成功", file=sys.stderr)
sys.stderr.flush()
break
except Exception as e:
if attempt < max_retries - 1:
print(f"数据库连接失败(尝试 {attempt + 1}/{max_retries}: {e}", file=sys.stderr)
print(f"等待 {retry_delay} 秒后重试...", file=sys.stderr)
sys.stderr.flush()
await asyncio.sleep(retry_delay)
else:
print(f"数据库连接失败,已达到最大重试次数: {e}", file=sys.stderr)
sys.stderr.flush()
# 不抛出异常,让应用继续启动,但数据库操作会失败
try:
startup_knowledge_base()
except Exception as e:
print(f"启动知识库服务失败: {e}", file=sys.stderr)
# 确保系统知识库与目录同步
try:
from src.core.database import get_db
from src.services.knowledge_base_service import KnowledgeBaseService
db = next(get_db())
kb_service = KnowledgeBaseService(db)
kb_service.ensure_system_knowledge_bases()
db.close()
except Exception as e:
print(f"同步系统知识库失败: {e}", file=sys.stderr)
# 初始化论坛分类
try:
seed_forum_categories()
except Exception as e:
print(f"初始化论坛分类失败: {e}", file=sys.stderr)
sys.stderr.flush()
print("应用启动事件完成", file=sys.stderr)
sys.stderr.flush()
# 应用关闭事件
@app.on_event("shutdown")
async def shutdown_event():
"""应用关闭时执行"""
try:
from src.services.file_watcher_service import stop_file_watcher
stop_file_watcher()
print("知识库文件监控服务已停止")
except Exception as e:
print(f"停止知识库文件监控服务失败: {str(e)}")
# 注册路由
app.include_router(auth.router)
app.include_router(chat.router)
# 导入并注册文档API
from src.api import document
app.include_router(document.router)
# 导入并注册图像API
from src.api import image
app.include_router(image.router)
# 导入并注册分析API
from src.api import analytics
app.include_router(analytics.router)
# 导入并注册知识库API(已合并knowledge.py的功能)
from src.api import knowledge_base
app.include_router(knowledge_base.router)
# 导入并注册课程内容API
from src.api import course_content, forum
app.include_router(course_content.router)
app.include_router(forum.router)
# 导入并注册后台管理API
from src.api import admin
app.include_router(admin.router)
# 静态文件服务
if os.path.exists(settings.upload_dir):
app.mount("/uploads", StaticFiles(directory=settings.upload_dir), name="uploads")
# 挂载生成图像目录
if os.path.exists(settings.generated_images_dir):
app.mount("/generated_images", StaticFiles(directory=settings.generated_images_dir), name="generated_images")
# 挂载PDF图片提取目录
IMAGES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "images")
os.makedirs(IMAGES_DIR, exist_ok=True)
app.mount("/images", StaticFiles(directory=IMAGES_DIR), name="images")
# 根路径
@app.get("/")
async def root():
"""根路径"""
return {
"message": f"欢迎使用{settings.app_name}",
"version": settings.app_version,
"docs": "/docs"
}
# 健康检查端点
@app.get("/health")
async def health_check():
"""健康检查端点"""
# 检查数据库连接
db_healthy = check_database_connection()
status = "healthy" if db_healthy else "unhealthy"
return {
"status": status,
"timestamp": datetime.now().isoformat(),
"version": settings.app_version,
"database": "connected" if db_healthy else "disconnected",
"services": {
"api": "running",
"database": "connected" if db_healthy else "disconnected"
}
}
# 系统状态端点
@app.get("/system/status")
async def system_status():
"""系统状态端点"""
db_info = get_database_info()
# 获取系统信息
import psutil
import platform
system_info = {
"platform": platform.platform(),
"python_version": platform.python_version(),
"cpu_count": psutil.cpu_count(),
"memory_total": psutil.virtual_memory().total,
"memory_available": psutil.virtual_memory().available,
}
# 检查磁盘使用情况
try:
disk_usage = psutil.disk_usage("/")._asdict()
system_info["disk_usage"] = disk_usage
except:
system_info["disk_usage"] = None
# 获取服务状态
services_status = {
"api": "running",
"database": db_info.get("type", "unknown"),
"vector_store": "available" if os.path.exists(settings.vector_store_path) else "unavailable"
}
return {
"status": "operational",
"timestamp": datetime.now().isoformat(),
"system": system_info,
"services": services_status,
"database": db_info,
"config": {
"app_name": settings.app_name,
"app_version": settings.app_version,
"debug": settings.debug,
"database_type": "postgresql" if is_postgresql_database() else "sqlite"
}
}
# 服务发现端点
@app.get("/services")
async def list_services():
"""列出所有服务"""
services = {
"backend": {
"name": "FastAPI后端服务",
"endpoint": f"http://localhost:{settings.port}",
"health": "/health",
"docs": "/docs"
},
"database": {
"name": "数据库服务",
"type": "postgresql" if is_postgresql_database() else "sqlite",
"status": "running"
},
"vectorizer": {
"name": "向量化服务",
"description": "独立文档向量化处理服务",
"status": "available"
},
"frontend": {
"name": "Next.js前端服务",
"endpoint": "http://localhost:8001",
"status": "external"
}
}
return {
"services": services,
"timestamp": datetime.now().isoformat()
}
if __name__ == "__main__":
import sys
print("=" * 50, file=sys.stderr)
print("启动 Uvicorn 服务器...", file=sys.stderr)
print(f"Host: {settings.host}, Port: {settings.port}", file=sys.stderr)
print("=" * 50, file=sys.stderr)
sys.stderr.flush()
uvicorn.run(
"main:app",
host=settings.host,
port=settings.port,
reload=settings.debug,
log_level=settings.log_level.lower(),
log_config=None # 使用默认日志配置
)