"""
LandPPT specific API endpoints
"""
from fastapi import APIRouter, HTTPException, UploadFile, File, Form, Request, Depends
from typing import List, Optional
from ..auth.middleware import get_current_user_required
from ..database.models import User
import uuid
import json
import logging
import re
from .models import (
PPTScenario, PPTGenerationRequest, PPTGenerationResponse,
PPTOutline, PPTProject, TodoBoard, ProjectListResponse,
FileUploadResponse, SlideContent, FileOutlineGenerationRequest,
FileOutlineGenerationResponse, TemplateSelectionRequest, TemplateSelectionResponse
)
from ..services.service_instances import ppt_service, get_ppt_service_for_user
from ..services.file_processor import FileProcessor
from ..services.deep_research_service import DEEPResearchService
from ..services.research_report_generator import ResearchReportGenerator
from ..core.config import ai_config, resolve_timeout_seconds
def filter_think_tags(content: str) -> str:
"""
Filter out think tags from content - supports multiple formats
"""
if not content:
return content
# Patterns for different think tag formats
# Note: We add capturing groups to preserve surrounding whitespace
patterns = [
r'(\s*)(\s*)', # ... with whitespace
r'(\s*)(\s*)', # with whitespace
r'(\s*)[\s\S]*?(\s*)', # ... with whitespace
]
filtered_content = content
# Keep applying patterns until no more matches (handles nested or multiple tags)
max_iterations = 10
for _ in range(max_iterations):
new_content = filtered_content
for pattern in patterns:
new_content = re.sub(pattern, r'\1\2', new_content, flags=re.IGNORECASE)
if new_content == filtered_content:
break
filtered_content = new_content
# Clean up whitespace
# Remove multiple consecutive empty lines (more than 2)
filtered_content = re.sub(r'\n\s*\n\s*\n+', '\n\n', filtered_content)
# Remove empty lines at the beginning and end
filtered_content = filtered_content.strip()
# Clean up extra spaces within lines (but preserve single spaces)
filtered_content = re.sub(r' {2,}', ' ', filtered_content)
return filtered_content
router = APIRouter()
logger = logging.getLogger(__name__)
file_processor = FileProcessor()
# Research services (lazy initialization)
_research_service = None
_report_generator = None
_enhanced_research_service = None
_enhanced_report_generator = None
def get_research_service():
"""Get research service instance (lazy initialization)"""
global _research_service
if _research_service is None:
try:
_research_service = DEEPResearchService()
logger.info("Research service initialized successfully")
except Exception as e:
logger.warning(f"Failed to initialize research service: {e}")
return _research_service
def reload_research_service():
"""Reload research service to pick up new configuration"""
global _research_service
logger.info("Reloading research service...")
if _research_service is not None:
try:
_research_service.reload_config()
logger.info("Research service configuration reloaded successfully")
# Verify the service is still available after reload
if not _research_service.is_available():
logger.warning("Research service is not available after reload, will recreate on next access")
_research_service = None
except Exception as e:
logger.warning(f"Failed to reload research service config: {e}")
# If reload fails, recreate the service
_research_service = None
else:
# If service doesn't exist, force recreation on next access
logger.info("Research service will be recreated on next access with new configuration")
def get_report_generator():
"""Get report generator instance (lazy initialization)"""
global _report_generator
if _report_generator is None:
try:
_report_generator = ResearchReportGenerator()
logger.info("Report generator initialized successfully")
except Exception as e:
logger.warning(f"Failed to initialize report generator: {e}")
return _report_generator
def get_enhanced_research_service():
"""Get enhanced research service instance (lazy initialization)"""
global _enhanced_research_service
if _enhanced_research_service is None:
try:
from ..services.research.enhanced_research_service import EnhancedResearchService
_enhanced_research_service = EnhancedResearchService()
logger.info("Enhanced research service initialized successfully")
except Exception as e:
logger.warning(f"Failed to initialize enhanced research service: {e}")
return _enhanced_research_service
def get_enhanced_report_generator():
"""Get enhanced report generator instance (lazy initialization)"""
global _enhanced_report_generator
if _enhanced_report_generator is None:
try:
from ..services.research.enhanced_report_generator import EnhancedReportGenerator
_enhanced_report_generator = EnhancedReportGenerator()
logger.info("Enhanced report generator initialized successfully")
except Exception as e:
logger.warning(f"Failed to initialize enhanced report generator: {e}")
return _enhanced_report_generator
@router.get("/health")
async def health_check():
"""Health check endpoint"""
return {
"status": "healthy",
"service": "LandPPT API",
"version": "0.1.0",
"ai_provider": ai_config.default_ai_provider,
"available_providers": ai_config.get_available_providers()
}
@router.get("/ai/providers")
async def get_ai_providers():
"""Get available AI providers"""
# Get all providers (built-in only)
all_providers = ai_config.get_available_providers()
return {
"default_provider": ai_config.default_ai_provider,
"available_providers": all_providers,
"provider_status": {
provider: ai_config.is_provider_available(provider)
for provider in all_providers
}
}
@router.post("/ai/providers/{provider_name}/test")
async def test_ai_provider(provider_name: str, request: Request):
"""Test a specific AI provider - uses frontend provided config if available"""
# Anthropic has its own dedicated proxy endpoint in routes.py
if provider_name == "anthropic":
raise HTTPException(
status_code=400,
detail="Anthropic testing is handled by a dedicated endpoint. Please use the frontend test function."
)
try:
import aiohttp
import json
# Try to get configuration from request body (if provided by frontend)
body = None
try:
body = await request.json()
except:
pass # No JSON body, use backend config
# Special handling for OpenAI provider with frontend config
if provider_name == "openai" and body:
base_url = body.get('base_url')
api_key = body.get('api_key')
model = body.get('model', 'gpt-4o')
if base_url and api_key:
# Use frontend provided config for OpenAI
logger.info(f"Testing OpenAI with frontend config: {base_url}")
# Ensure base URL ends with /v1
if not base_url.endswith('/v1'):
base_url = base_url.rstrip('/') + '/v1'
chat_url = f"{base_url}/chat/completions"
timeout_seconds = resolve_timeout_seconds(
body.get("llm_timeout_seconds") if isinstance(body, dict) else None,
ai_config.llm_timeout_seconds,
)
async with aiohttp.ClientSession() as session:
headers = {
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json'
}
payload = {
"model": model,
"messages": [
{
"role": "user",
"content": "Say 'Hello, I am working!' in exactly 5 words."
}
],
"temperature": 0
}
async with session.post(
chat_url,
headers=headers,
json=payload,
timeout=aiohttp.ClientTimeout(total=timeout_seconds),
) as response:
if response.status == 200:
data = await response.json()
# Apply think tag filtering to the response
raw_content = data['choices'][0]['message']['content']
filtered_content = filter_think_tags(raw_content)
return {
"provider": provider_name,
"status": "success",
"model": model,
"response_preview": filtered_content,
"usage": data.get('usage', {})
}
else:
error_text = await response.text()
raise HTTPException(status_code=response.status, detail=f"API error: {error_text}")
# Fallback to backend config for other providers or when no frontend config
from ..ai import AIProviderFactory, AIMessage, MessageRole
if not ai_config.is_provider_available(provider_name):
raise HTTPException(status_code=400, detail=f"Provider {provider_name} is not available")
# Create provider instance with backend config
provider = AIProviderFactory.create_provider(provider_name)
# Test with a simple message
test_message = AIMessage(
role=MessageRole.USER,
content="Hello, please respond with a brief greeting."
)
response = await provider.chat_completion([test_message])
# Apply think tag filtering to the response content
filtered_content = filter_think_tags(response.content)
return {
"provider": provider_name,
"status": "success",
"model": response.model,
"response_preview": filtered_content[:100] + "..." if len(filtered_content) > 100 else filtered_content,
"usage": response.usage
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Provider test failed: {str(e)}")
raise HTTPException(status_code=500, detail=f"Provider test failed: {str(e)}")
@router.get("/scenarios", response_model=List[PPTScenario])
async def get_scenarios():
"""Get available PPT scenarios - Updated to match requires.md specifications"""
return [
PPTScenario(
id="general",
name="通用",
description="适用于各种通用场景的PPT模板,提供专业的商务风格",
icon="📋",
template_config={"style": "professional", "color_scheme": "blue", "font_family": "Arial, sans-serif"}
),
PPTScenario(
id="tourism",
name="旅游观光",
description="旅游线路策划、景点介绍、行程规划等旅游相关内容",
icon="🌍",
template_config={"style": "vibrant", "color_scheme": "green", "font_family": "Georgia, serif"}
),
PPTScenario(
id="education",
name="儿童科普",
description="教育培训、科普知识、儿童友好的设计风格",
icon="🎓",
template_config={"style": "playful", "color_scheme": "rainbow", "font_family": "Comic Sans MS, cursive"}
),
PPTScenario(
id="analysis",
name="深入分析",
description="数据分析、研究报告、学术论文等专业分析内容",
icon="📊",
template_config={"style": "analytical", "color_scheme": "dark", "font_family": "Helvetica, sans-serif"}
),
PPTScenario(
id="history",
name="历史文化",
description="历史事件、文化传承、人文艺术等主题",
icon="🏛️",
template_config={"style": "classical", "color_scheme": "brown", "font_family": "Times New Roman, serif"}
),
PPTScenario(
id="technology",
name="科技技术",
description="技术介绍、产品发布、创新展示等科技内容",
icon="💻",
template_config={"style": "modern", "color_scheme": "purple", "font_family": "Roboto, sans-serif"}
),
PPTScenario(
id="business",
name="方案汇报",
description="商业计划、项目汇报、企业展示等商务场景",
icon="💼",
template_config={"style": "corporate", "color_scheme": "navy", "font_family": "Arial, sans-serif"}
)
]
# Legacy PPT generation endpoint removed - now using project-based workflow
# Use POST /projects to create a new project instead
@router.post("/upload")
async def upload_file(file: UploadFile = File(...)):
"""Upload document for PPT generation"""
try:
# Validate file type
allowed_types = [".docx", ".pdf", ".txt", ".md"]
file_extension = "." + file.filename.split(".")[-1].lower()
if file_extension not in allowed_types:
raise HTTPException(
status_code=400,
detail=f"Unsupported file type. Allowed types: {', '.join(allowed_types)}"
)
# Read file content
content = await file.read()
# Process file based on type
processed_content = await ppt_service.process_uploaded_file(
filename=file.filename,
content=content,
file_type=file_extension
)
return {
"filename": file.filename,
"size": len(content),
"type": file_extension,
"processed_content": processed_content,
"message": "File uploaded and processed successfully"
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error processing file: {str(e)}")
# Legacy task management endpoints removed - now using project-based workflow
# Use /projects endpoints for project management instead
@router.post("/outline/generate")
async def generate_outline(request: PPTGenerationRequest):
"""Generate PPT outline only"""
try:
outline = await ppt_service.generate_outline(request)
return {"outline": outline, "status": "success"}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error generating outline: {str(e)}")
# New Project Management Endpoints
@router.post("/projects", response_model=PPTProject)
async def create_project(
request: PPTGenerationRequest,
user: User = Depends(get_current_user_required)
):
"""Create a new PPT project with TODO workflow"""
try:
# Ensure user_id is set from authenticated user
request.user_id = user.id
user_ppt_service = get_ppt_service_for_user(user.id)
project = await user_ppt_service.create_project_with_workflow(request)
return project
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error creating project: {str(e)}")
@router.get("/projects", response_model=ProjectListResponse)
async def list_projects(
page: int = 1,
page_size: int = 10,
status: Optional[str] = None,
user: User = Depends(get_current_user_required)
):
"""List projects with pagination - only returns projects owned by current user"""
try:
user_ppt_service = get_ppt_service_for_user(user.id)
return await user_ppt_service.project_manager.list_projects(
page=page, page_size=page_size, status=status, user_id=user.id
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error listing projects: {str(e)}")
@router.get("/projects/{project_id}", response_model=PPTProject)
async def get_project(
project_id: str,
user: User = Depends(get_current_user_required)
):
"""Get project details - enforces user ownership"""
try:
user_ppt_service = get_ppt_service_for_user(user.id)
project = await user_ppt_service.project_manager.get_project(project_id, user_id=user.id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
return project
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error getting project: {str(e)}")
@router.get("/projects/{project_id}/todo", response_model=TodoBoard)
async def get_project_todo_board(
project_id: str,
user: User = Depends(get_current_user_required)
):
"""Get TODO board for a project - enforces user ownership"""
try:
user_ppt_service = get_ppt_service_for_user(user.id)
todo_board = await user_ppt_service.get_project_todo_board(project_id, user_id=user.id)
if not todo_board:
raise HTTPException(status_code=404, detail="TODO board not found")
return todo_board
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error getting TODO board: {str(e)}")
@router.put("/projects/{project_id}/stages/{stage_id}")
async def update_project_stage(
project_id: str,
stage_id: str,
request: Request,
user: User = Depends(get_current_user_required)
):
"""Update project stage status - enforces user ownership"""
try:
# Parse JSON body
body = await request.json()
status = body.get("status")
progress = body.get("progress")
if not status:
raise HTTPException(status_code=422, detail="Status is required")
user_ppt_service = get_ppt_service_for_user(user.id)
success = await user_ppt_service.update_project_stage(
project_id, stage_id, status, progress, user_id=user.id
)
if not success:
raise HTTPException(status_code=404, detail="Project or stage not found")
return {"status": "success", "message": "Stage updated successfully"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error updating stage: {str(e)}")
@router.post("/projects/{project_id}/continue-from-stage")
async def continue_from_stage(
project_id: str,
request: Request,
user: User = Depends(get_current_user_required)
):
"""Continue project workflow from a specific stage - enforces user ownership"""
try:
# Parse JSON body
body = await request.json()
stage_id = body.get("stage_id")
if not stage_id:
raise HTTPException(status_code=422, detail="Stage ID is required")
# Get project with user ownership check
user_ppt_service = get_ppt_service_for_user(user.id)
project = await user_ppt_service.project_manager.get_project(project_id, user_id=user.id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
# Reset stages from the specified stage onwards
success = await user_ppt_service.reset_stages_from(project_id, stage_id, user_id=user.id)
if not success:
raise HTTPException(status_code=400, detail="Failed to reset stages")
# Start workflow from the specified stage
await user_ppt_service.start_workflow_from_stage(project_id, stage_id, user_id=user.id)
return {
"status": "success",
"message": f"Workflow restarted from stage: {stage_id}",
"project_id": project_id,
"stage_id": stage_id
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error continuing from stage: {str(e)}")
@router.post("/projects/{project_id}/slides/{slide_index}/lock")
async def lock_slide(
project_id: str,
slide_index: int,
user: User = Depends(get_current_user_required)
):
"""Lock a slide to prevent regeneration - enforces user ownership"""
try:
user_ppt_service = get_ppt_service_for_user(user.id)
success = await user_ppt_service.lock_slide(project_id, slide_index, user_id=user.id)
if not success:
raise HTTPException(status_code=404, detail="Slide not found")
return {"status": "success", "message": "Slide locked successfully"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error locking slide: {str(e)}")
@router.post("/projects/{project_id}/slides/{slide_index}/unlock")
async def unlock_slide(
project_id: str,
slide_index: int,
user: User = Depends(get_current_user_required)
):
"""Unlock a slide to allow regeneration - enforces user ownership"""
try:
user_ppt_service = get_ppt_service_for_user(user.id)
success = await user_ppt_service.unlock_slide(project_id, slide_index, user_id=user.id)
if not success:
raise HTTPException(status_code=404, detail="Slide not found")
return {"status": "success", "message": "Slide unlocked successfully"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error unlocking slide: {str(e)}")
@router.get("/projects/{project_id}/versions")
async def get_project_versions(
project_id: str,
user: User = Depends(get_current_user_required)
):
"""Get all versions of a project - enforces user ownership"""
try:
user_ppt_service = get_ppt_service_for_user(user.id)
versions = await user_ppt_service.project_manager.get_project_versions(project_id, user_id=user.id)
return {"versions": versions, "status": "success"}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error getting project versions: {str(e)}")
@router.post("/projects/{project_id}/versions/{version}/restore")
async def restore_project_version(
project_id: str,
version: int,
user: User = Depends(get_current_user_required)
):
"""Restore project to a specific version - enforces user ownership"""
try:
user_ppt_service = get_ppt_service_for_user(user.id)
success = await user_ppt_service.project_manager.restore_project_version(
project_id, version, user_id=user.id
)
if not success:
raise HTTPException(status_code=404, detail="Project or version not found")
return {"status": "success", "message": "Project restored successfully"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error restoring project version: {str(e)}")
@router.delete("/projects/{project_id}")
async def delete_project(
project_id: str,
user: User = Depends(get_current_user_required)
):
"""Delete a project - enforces user ownership"""
try:
user_ppt_service = get_ppt_service_for_user(user.id)
success = await user_ppt_service.project_manager.delete_project(project_id, user_id=user.id)
if not success:
raise HTTPException(status_code=404, detail="Project not found")
return {"status": "success", "message": "Project deleted successfully"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error deleting project: {str(e)}")
@router.post("/projects/{project_id}/archive")
async def archive_project(
project_id: str,
user: User = Depends(get_current_user_required)
):
"""Archive a project - enforces user ownership"""
try:
user_ppt_service = get_ppt_service_for_user(user.id)
success = await user_ppt_service.project_manager.archive_project(project_id, user_id=user.id)
if not success:
raise HTTPException(status_code=404, detail="Project not found")
return {"status": "success", "message": "Project archived successfully"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error archiving project: {str(e)}")
# File Upload Endpoints
@router.post("/upload", response_model=FileUploadResponse)
async def upload_file(file: UploadFile = File(...)):
"""Upload and process document file"""
try:
# Validate file
is_valid, message = file_processor.validate_file(file.filename, file.size)
if not is_valid:
raise HTTPException(status_code=400, detail=message)
# Save uploaded file temporarily
import tempfile
import os
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(file.filename)[1]) as temp_file:
content = await file.read()
temp_file.write(content)
temp_file_path = temp_file.name
try:
# Process the file
result = await file_processor.process_file(temp_file_path, file.filename)
return result
finally:
# Clean up temporary file
if os.path.exists(temp_file_path):
os.unlink(temp_file_path)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error processing file: {str(e)}")
@router.get("/upload/formats")
async def get_supported_formats():
"""Get supported file formats"""
try:
formats = file_processor.get_supported_formats()
return {
"supported_formats": formats,
"max_size_mb": 100,
"description": "支持的文件格式和上传限制"
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error getting formats: {str(e)}")
@router.post("/upload/create-project", response_model=PPTProject)
async def create_project_from_upload(
file: UploadFile = File(...),
topic: Optional[str] = Form(None),
scenario: Optional[str] = Form(None),
requirements: Optional[str] = Form(None),
language: str = Form("zh"),
user: User = Depends(get_current_user_required)
):
"""Upload file and create project directly - associates with current user"""
try:
# Validate and process file
is_valid, message = file_processor.validate_file(file.filename, file.size)
if not is_valid:
raise HTTPException(status_code=400, detail=message)
# Save and process file
import tempfile
import os
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(file.filename)[1]) as temp_file:
content = await file.read()
temp_file.write(content)
temp_file_path = temp_file.name
try:
# Process file
file_result = await file_processor.process_file(temp_file_path, file.filename)
# Create PPT request from content
ppt_data = await file_processor.create_ppt_from_content(
file_result.processed_content,
topic
)
# Override with user inputs if provided
if topic:
ppt_data['topic'] = topic
if scenario:
ppt_data['scenario'] = scenario
if requirements:
ppt_data['requirements'] = requirements
# Create project request with user_id
project_request = PPTGenerationRequest(**ppt_data)
project_request.user_id = user.id
# Create project with workflow
user_ppt_service = get_ppt_service_for_user(user.id)
project = await user_ppt_service.create_project_with_workflow(project_request)
return project
finally:
# Clean up temporary file
if os.path.exists(temp_file_path):
os.unlink(temp_file_path)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error creating project from upload: {str(e)}")
@router.post("/upload/analyze")
async def analyze_uploaded_content(file: UploadFile = File(...)):
"""Analyze uploaded file and suggest PPT structure"""
try:
# Validate file
is_valid, message = file_processor.validate_file(file.filename, file.size)
if not is_valid:
raise HTTPException(status_code=400, detail=message)
# Save and process file
import tempfile
import os
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(file.filename)[1]) as temp_file:
content = await file.read()
temp_file.write(content)
temp_file_path = temp_file.name
try:
# Process file
file_result = await file_processor.process_file(temp_file_path, file.filename)
# Create suggested PPT structure
ppt_suggestion = await file_processor.create_ppt_from_content(file_result.processed_content)
return {
"file_info": file_result,
"ppt_suggestion": ppt_suggestion,
"status": "success"
}
finally:
# Clean up temporary file
if os.path.exists(temp_file_path):
os.unlink(temp_file_path)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error analyzing file: {str(e)}")
@router.post("/slides/generate")
async def generate_slides(outline: PPTOutline, scenario: str = "general"):
"""Generate slides from outline"""
try:
slides_html = await ppt_service.generate_slides_from_outline(outline, scenario)
return {"slides_html": slides_html, "status": "success"}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error generating slides: {str(e)}")
@router.post("/files/generate-outline", response_model=FileOutlineGenerationResponse)
async def generate_outline_from_file(request: FileOutlineGenerationRequest):
"""使用summeryanyfile从文件生成PPT大纲"""
try:
# 调用增强的PPT服务来生成大纲
result = await ppt_service.generate_outline_from_file(request)
return result
except Exception as e:
logger.error(f"Error generating outline from file: {e}")
return FileOutlineGenerationResponse(
success=False,
error=str(e),
message=f"从文件生成大纲失败: {str(e)}"
)
@router.post("/files/upload-and-generate-outline")
async def upload_file_and_generate_outline(
files: List[UploadFile] = File(...),
topic: Optional[str] = Form(None),
scenario: str = Form("general"),
page_count_mode: str = Form("ai_decide"),
min_pages: int = Form(8),
max_pages: int = Form(15),
fixed_pages: int = Form(10),
ppt_style: str = Form("general"),
custom_style_prompt: Optional[str] = Form(None),
file_processing_mode: str = Form("markitdown"),
content_analysis_depth: str = Form("standard"),
focus_content: Optional[str] = Form(None),
tech_highlights: Optional[str] = Form(None),
target_audience: Optional[str] = Form(None),
network_mode: bool = Form(False), # 是否启用联网搜索(与项目创建保持一致)
language: str = Form("zh") # 语言参数
):
"""上传多个文件并直接生成PPT大纲,支持联网搜索集成"""
try:
# 验证所有文件
for file in files:
is_valid, message = file_processor.validate_file(file.filename, file.size)
if not is_valid:
raise HTTPException(status_code=400, detail=f"{file.filename}: {message}")
# 保存所有临时文件并处理
import tempfile
import os
temp_file_paths = []
all_processed_content = []
try:
# 保存并处理每个文件
for file in files:
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(file.filename)[1]) as temp_file:
content = await file.read()
temp_file.write(content)
temp_file_path = temp_file.name
temp_file_paths.append(temp_file_path)
# 处理单个文件
file_result = await file_processor.process_file(
temp_file_path,
file.filename,
file_processing_mode=file_processing_mode,
)
all_processed_content.append({
"filename": file.filename,
"content": file_result.processed_content
})
# 如果启用联网搜索,先进行搜索并整合
merged_file_path = None
if network_mode and topic:
logger.info(f"启用联网搜索模式,主题: {topic}")
# 构建上下文信息
context = {
'scenario': scenario,
'target_audience': target_audience or '普通大众',
'requirements': '',
'ppt_style': ppt_style,
'description': f'文件数量: {len(files)}',
'file_processing_mode': file_processing_mode,
}
# 获取所有文件路径
file_paths_for_merge = [path for path in temp_file_paths if not path.endswith('.md')]
# 进行联网搜索并与文件整合
merged_file_path = await ppt_service.conduct_research_and_merge_with_files(
topic=topic,
language=language,
file_paths=file_paths_for_merge,
context=context
)
temp_file_paths.append(merged_file_path)
logger.info(f"✅ 联网搜索和文件整合完成: {merged_file_path}")
# 使用整合后的文件作为最终文件
final_filename = f"merged_with_search_{len(files)}_files.md"
else:
# 不使用联网搜索,仅合并所有文件内容
merged_content = file_processor.merge_multiple_files_to_markdown(all_processed_content)
# 创建临时合并文件
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.md', encoding='utf-8') as merged_file:
merged_file.write(merged_content)
merged_file_path = merged_file.name
temp_file_paths.append(merged_file_path)
final_filename = f"merged_content_{len(files)}_files.md"
# 解析多选字段
focus_content_list = focus_content.split(',') if focus_content else []
tech_highlights_list = tech_highlights.split(',') if tech_highlights else []
# 创建请求对象,使用合并后的文件
outline_request = FileOutlineGenerationRequest(
file_path=merged_file_path,
filename=final_filename,
topic=topic,
scenario=scenario,
requirements="", # API调用暂时没有requirements参数
page_count_mode=page_count_mode,
min_pages=min_pages,
max_pages=max_pages,
fixed_pages=fixed_pages,
ppt_style=ppt_style,
custom_style_prompt=custom_style_prompt,
file_processing_mode=file_processing_mode,
content_analysis_depth=content_analysis_depth,
target_audience=target_audience,
language=language
)
# 生成大纲
result = await ppt_service.generate_outline_from_file(outline_request)
# 在结果中添加文件列表信息
if result.success and result.metadata:
result.metadata["source_files"] = [f.filename for f in files]
result.metadata["files_count"] = len(files)
result.metadata["network_mode"] = network_mode
if network_mode:
result.metadata["search_topic"] = topic
return result
finally:
# 清理所有临时文件
for temp_path in temp_file_paths:
if os.path.exists(temp_path):
try:
os.unlink(temp_path)
except Exception as e:
logger.warning(f"Failed to delete temp file {temp_path}: {e}")
except HTTPException:
raise
except Exception as e:
logger.error(f"Error uploading files and generating outline: {e}")
return FileOutlineGenerationResponse(
success=False,
error=str(e),
message=f"文件上传和大纲生成失败: {str(e)}"
)
@router.post("/projects/{project_id}/select-template", response_model=TemplateSelectionResponse)
async def select_global_template_for_project(
project_id: str,
request: TemplateSelectionRequest,
user: User = Depends(get_current_user_required)
):
"""为项目选择全局母版模板 - enforces user ownership"""
try:
# 验证项目ID匹配
if request.project_id != project_id:
raise HTTPException(status_code=400, detail="Project ID mismatch")
# 选择模板 with user ownership check
user_ppt_service = get_ppt_service_for_user(user.id)
if request.template_mode == "free":
result = await user_ppt_service.select_free_template_for_project(project_id, user_id=user.id)
else:
# "default" 和未指定都等价于 selected_template_id=None(由后端选择默认模板)
template_id = None if request.template_mode == "default" else request.selected_template_id
result = await user_ppt_service.select_global_template_for_project(project_id, template_id, user_id=user.id)
if not result['success']:
raise HTTPException(status_code=400, detail=result['message'])
return TemplateSelectionResponse(**result)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to select template: {str(e)}")
@router.get("/projects/{project_id}/selected-template")
async def get_selected_global_template(
project_id: str,
user: User = Depends(get_current_user_required)
):
"""获取项目选择的全局母版模板 - enforces user ownership"""
try:
user_ppt_service = get_ppt_service_for_user(user.id)
template = await user_ppt_service.get_selected_global_template(project_id, user_id=user.id)
if not template:
return {"selected_template": None, "message": "No template selected"}
return {"selected_template": template}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get selected template: {str(e)}")
# 文件缓存管理端点
@router.get("/cache/stats")
async def get_file_cache_stats():
"""获取文件缓存统计信息"""
try:
stats = ppt_service.get_cache_stats()
return {"success": True, "stats": stats}
except Exception as e:
logger.error(f"Error getting cache stats: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/cache/cleanup")
async def cleanup_file_cache():
"""清理过期的文件缓存条目"""
try:
ppt_service.cleanup_cache()
return {"success": True, "message": "文件缓存清理完成"}
except Exception as e:
logger.error(f"Error cleaning up cache: {e}")
raise HTTPException(status_code=500, detail=str(e))
# DEEP Research Endpoints
@router.get("/research/status")
async def get_research_status():
"""Get research service status"""
try:
research_service = get_research_service()
if not research_service:
return {
"available": False,
"message": "Research service not initialized",
"tavily_configured": bool(ai_config.tavily_api_key)
}
status = research_service.get_status()
return {
"available": research_service.is_available(),
"status": status,
"tavily_configured": bool(ai_config.tavily_api_key)
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error getting research status: {str(e)}")
@router.post("/research/conduct")
async def conduct_research(topic: str, language: str = "zh"):
"""Conduct DEEP research on a topic"""
try:
research_service = get_research_service()
if not research_service or not research_service.is_available():
raise HTTPException(
status_code=503,
detail="Research service not available. Please check Tavily API configuration."
)
# Conduct research
research_report = await research_service.conduct_deep_research(topic, language)
# Save report if generator is available
report_path = None
report_generator = get_report_generator()
if report_generator:
try:
report_path = report_generator.save_report_to_file(research_report)
except Exception as save_error:
logger.warning(f"Failed to save research report: {save_error}")
return {
"success": True,
"report": {
"topic": research_report.topic,
"language": research_report.language,
"executive_summary": research_report.executive_summary,
"key_findings": research_report.key_findings,
"recommendations": research_report.recommendations,
"total_duration": research_report.total_duration,
"sources_count": len(research_report.sources),
"steps_count": len(research_report.steps)
},
"report_path": report_path,
"message": "Research completed successfully"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error conducting research: {e}")
raise HTTPException(status_code=500, detail=f"Research failed: {str(e)}")
@router.get("/research/reports")
async def list_research_reports():
"""List all saved research reports"""
try:
report_generator = get_report_generator()
if not report_generator:
raise HTTPException(
status_code=503,
detail="Report generator not available"
)
reports = report_generator.list_saved_reports()
return {
"success": True,
"reports": reports,
"total_count": len(reports),
"reports_directory": report_generator.get_reports_directory()
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error listing reports: {str(e)}")
@router.delete("/research/reports/{filename}")
async def delete_research_report(filename: str):
"""Delete a research report"""
try:
report_generator = get_report_generator()
if not report_generator:
raise HTTPException(
status_code=503,
detail="Report generator not available"
)
success = report_generator.delete_report(filename)
if not success:
raise HTTPException(status_code=404, detail="Report not found")
return {
"success": True,
"message": f"Report {filename} deleted successfully"
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error deleting report: {str(e)}")
@router.get("/research/reports/directory")
async def get_reports_directory():
"""Get the reports directory path"""
try:
report_generator = get_report_generator()
if not report_generator:
raise HTTPException(
status_code=503,
detail="Report generator not available"
)
directory = report_generator.get_reports_directory()
return {
"success": True,
"directory": directory,
"message": "Reports are saved in this directory"
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error getting directory: {str(e)}")
# Enhanced Research API Endpoints
@router.post("/research/enhanced/conduct")
async def conduct_enhanced_research(topic: str, language: str = "zh"):
"""Conduct enhanced research using multiple providers and deep content analysis"""
try:
enhanced_service = get_enhanced_research_service()
if not enhanced_service or not enhanced_service.is_available():
raise HTTPException(
status_code=503,
detail="Enhanced research service not available. Please check provider configurations."
)
# Conduct enhanced research
enhanced_report = await enhanced_service.conduct_enhanced_research(topic, language)
# Save enhanced report
report_path = None
enhanced_generator = get_enhanced_report_generator()
if enhanced_generator:
try:
report_path = enhanced_generator.save_report_to_file(enhanced_report)
except Exception as save_error:
logger.warning(f"Failed to save enhanced research report: {save_error}")
return {
"success": True,
"message": "Enhanced research completed successfully",
"report": {
"topic": enhanced_report.topic,
"language": enhanced_report.language,
"executive_summary": enhanced_report.executive_summary,
"key_findings": enhanced_report.key_findings,
"recommendations": enhanced_report.recommendations,
"sources": enhanced_report.sources,
"created_at": enhanced_report.created_at.isoformat(),
"total_duration": enhanced_report.total_duration,
"steps_count": len(enhanced_report.steps),
"provider_stats": enhanced_report.provider_stats,
"content_analysis": enhanced_report.content_analysis
},
"report_path": report_path
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Enhanced research failed: {e}")
raise HTTPException(status_code=500, detail=f"Enhanced research failed: {str(e)}")
@router.get("/research/enhanced/status")
async def get_enhanced_research_status():
"""Get enhanced research service status and configuration"""
try:
enhanced_service = get_enhanced_research_service()
if not enhanced_service:
return {
"available": False,
"message": "Enhanced research service not initialized"
}
status = enhanced_service.get_status()
return {
"success": True,
"status": status
}
except Exception as e:
logger.error(f"Error getting enhanced research status: {e}")
raise HTTPException(status_code=500, detail=f"Error getting enhanced research status: {str(e)}")
@router.get("/research/enhanced/providers")
async def get_available_research_providers():
"""Get list of available research providers"""
try:
enhanced_service = get_enhanced_research_service()
providers = {
"tavily": False,
"searxng": False,
"content_extraction": False
}
if enhanced_service:
available_providers = enhanced_service.get_available_providers()
for provider in available_providers:
if provider in providers:
providers[provider] = True
# Check content extraction
from ...core.config import ai_config
providers["content_extraction"] = ai_config.research_enable_content_extraction
return {
"success": True,
"providers": providers,
"enhanced_service_available": enhanced_service is not None and enhanced_service.is_available()
}
except Exception as e:
logger.error(f"Error getting research providers: {e}")
raise HTTPException(status_code=500, detail=f"Error getting research providers: {str(e)}")