| """ |
| 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 = [ |
| r'(\s*)<think[\s\S]*?></think>(\s*)', |
| r'(\s*)<think[\s\S]*?/>(\s*)', |
| r'(\s*)<think>[\s\S]*?</think>(\s*)', |
| ] |
|
|
| filtered_content = content |
| |
| 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 |
|
|
| |
| |
| filtered_content = re.sub(r'\n\s*\n\s*\n+', '\n\n', filtered_content) |
|
|
| |
| filtered_content = filtered_content.strip() |
|
|
| |
| filtered_content = re.sub(r' {2,}', ' ', filtered_content) |
|
|
| return filtered_content |
|
|
|
|
| router = APIRouter() |
| logger = logging.getLogger(__name__) |
| file_processor = FileProcessor() |
|
|
| |
| _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") |
|
|
| |
| 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}") |
| |
| _research_service = None |
| else: |
| |
| 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""" |
| |
| 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""" |
| |
| 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 |
|
|
| |
| body = None |
| try: |
| body = await request.json() |
| except: |
| pass |
|
|
| |
| 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: |
| |
| logger.info(f"Testing OpenAI with frontend config: {base_url}") |
| |
| |
| 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() |
| |
| 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}") |
| |
| |
| 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") |
|
|
| |
| provider = AIProviderFactory.create_provider(provider_name) |
|
|
| |
| test_message = AIMessage( |
| role=MessageRole.USER, |
| content="Hello, please respond with a brief greeting." |
| ) |
|
|
| response = await provider.chat_completion([test_message]) |
|
|
| |
| 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"} |
| ) |
| ] |
|
|
| |
| |
|
|
| @router.post("/upload") |
| async def upload_file(file: UploadFile = File(...)): |
| """Upload document for PPT generation""" |
| try: |
| |
| 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)}" |
| ) |
| |
| |
| content = await file.read() |
| |
| |
| 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)}") |
|
|
| |
| |
|
|
| @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)}") |
|
|
| |
|
|
| @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: |
| |
| 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: |
| |
| 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: |
| |
| body = await request.json() |
| stage_id = body.get("stage_id") |
|
|
| if not stage_id: |
| raise HTTPException(status_code=422, detail="Stage ID is required") |
|
|
| |
| 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") |
|
|
| |
| 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") |
|
|
| |
| 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)}") |
|
|
| |
|
|
| @router.post("/upload", response_model=FileUploadResponse) |
| async def upload_file(file: UploadFile = File(...)): |
| """Upload and process document file""" |
| try: |
| |
| is_valid, message = file_processor.validate_file(file.filename, file.size) |
| if not is_valid: |
| raise HTTPException(status_code=400, detail=message) |
|
|
| |
| 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: |
| |
| result = await file_processor.process_file(temp_file_path, file.filename) |
| return result |
|
|
| finally: |
| |
| 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: |
| |
| is_valid, message = file_processor.validate_file(file.filename, file.size) |
| if not is_valid: |
| raise HTTPException(status_code=400, detail=message) |
|
|
| |
| 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: |
| |
| file_result = await file_processor.process_file(temp_file_path, file.filename) |
|
|
| |
| ppt_data = await file_processor.create_ppt_from_content( |
| file_result.processed_content, |
| topic |
| ) |
|
|
| |
| if topic: |
| ppt_data['topic'] = topic |
| if scenario: |
| ppt_data['scenario'] = scenario |
| if requirements: |
| ppt_data['requirements'] = requirements |
|
|
| |
| project_request = PPTGenerationRequest(**ppt_data) |
| project_request.user_id = user.id |
|
|
| |
| user_ppt_service = get_ppt_service_for_user(user.id) |
| project = await user_ppt_service.create_project_with_workflow(project_request) |
|
|
| return project |
|
|
| finally: |
| |
| 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: |
| |
| is_valid, message = file_processor.validate_file(file.filename, file.size) |
| if not is_valid: |
| raise HTTPException(status_code=400, detail=message) |
|
|
| |
| 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: |
| |
| file_result = await file_processor.process_file(temp_file_path, file.filename) |
|
|
| |
| 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: |
| |
| 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: |
| |
| 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="", |
| 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: |
| |
| if request.project_id != project_id: |
| raise HTTPException(status_code=400, detail="Project ID mismatch") |
|
|
| |
| 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: |
| |
| 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)) |
|
|
| |
|
|
| @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." |
| ) |
|
|
| |
| research_report = await research_service.conduct_deep_research(topic, language) |
|
|
| |
| 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)}") |
|
|
|
|
| |
|
|
| @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." |
| ) |
|
|
| |
| enhanced_report = await enhanced_service.conduct_enhanced_research(topic, language) |
|
|
| |
| 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 |
|
|
| |
| 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)}") |
|
|