| """ |
| SAAP Document Upload API |
| Handles file uploads, parsing, and privacy detection for multi-agent chat |
| """ |
| import logging |
| from typing import Optional |
| from fastapi import APIRouter, UploadFile, File, HTTPException, Form |
| from fastapi.responses import JSONResponse |
| from pydantic import BaseModel |
|
|
| from services.document_parser import document_parser |
| from services.privacy_detector import privacy_detector |
|
|
| logger = logging.getLogger(__name__) |
|
|
| router = APIRouter(prefix="/api/v1/documents", tags=["documents"]) |
|
|
|
|
| class DocumentUploadResponse(BaseModel): |
| """Response model for document upload""" |
| success: bool |
| document_id: str |
| filename: str |
| file_type: str |
| file_size: int |
| char_count: int |
| has_sensitive_data: bool |
| sensitive_data_types: list[str] |
| content_preview: str |
| full_content: str |
| error: Optional[str] = None |
|
|
|
|
| @router.post("/upload", response_model=DocumentUploadResponse) |
| async def upload_document( |
| file: UploadFile = File(...), |
| user_message: Optional[str] = Form(None) |
| ): |
| """ |
| Upload and parse a document (PDF, DOCX, TXT) |
| |
| - Extracts text content from document |
| - Analyzes for sensitive/private data |
| - Returns parsed content and privacy analysis |
| |
| Args: |
| file: Uploaded file (PDF, DOCX, TXT) |
| user_message: Optional user message with context about the document |
| |
| Returns: |
| DocumentUploadResponse with parsed content and privacy analysis |
| """ |
| try: |
| logger.info(f"π€ Document upload started: {file.filename}") |
| |
| |
| file_content = await file.read() |
| |
| |
| parse_result = document_parser.parse_document( |
| file_data=file_content, |
| filename=file.filename, |
| mime_type=file.content_type |
| ) |
| |
| if not parse_result["success"]: |
| logger.error(f"β Document parsing failed: {parse_result['error']}") |
| raise HTTPException( |
| status_code=400, |
| detail=f"Failed to parse document: {parse_result['error']}" |
| ) |
| |
| extracted_text = parse_result["content"] |
| metadata = parse_result["metadata"] |
| |
| |
| from services.privacy_detector import analyze_document_privacy |
| privacy_level, privacy_details = analyze_document_privacy( |
| document_text=extracted_text, |
| filename=file.filename |
| ) |
| |
| |
| has_sensitive_data = privacy_level.value in ["private", "confidential"] |
| |
| |
| sensitive_types = [] |
| if privacy_details.get("keyword_matches"): |
| sensitive_types.extend([cat for cat, _ in privacy_details["keyword_matches"]]) |
| if privacy_details.get("pattern_matches"): |
| sensitive_types.extend([cat for _, cat in privacy_details["pattern_matches"]]) |
| if privacy_details.get("document_indicators"): |
| sensitive_types.extend(privacy_details["document_indicators"]) |
| |
| |
| sensitive_types = list(set(sensitive_types)) |
| |
| |
| content_preview = extracted_text[:500] |
| if len(extracted_text) > 500: |
| content_preview += "..." |
| |
| |
| import hashlib |
| doc_id = hashlib.sha256(file_content).hexdigest()[:16] |
| |
| logger.info( |
| f"β
Document processed successfully: {file.filename} " |
| f"({metadata['char_count']} chars, " |
| f"sensitive: {has_sensitive_data})" |
| ) |
| |
| return DocumentUploadResponse( |
| success=True, |
| document_id=doc_id, |
| filename=metadata["filename"], |
| file_type=metadata["file_type"], |
| file_size=metadata["file_size"], |
| char_count=metadata["char_count"], |
| has_sensitive_data=has_sensitive_data, |
| sensitive_data_types=sensitive_types, |
| content_preview=content_preview, |
| full_content=extracted_text, |
| error=None |
| ) |
| |
| except HTTPException: |
| raise |
| |
| except Exception as e: |
| logger.error(f"β Document upload error: {e}", exc_info=True) |
| raise HTTPException( |
| status_code=500, |
| detail=f"Internal server error: {str(e)}" |
| ) |
|
|
|
|
| @router.post("/analyze") |
| async def analyze_document_content( |
| file: UploadFile = File(...), |
| ): |
| """ |
| Analyze document for sensitive data without full parsing |
| |
| Quick privacy check before full processing |
| |
| Args: |
| file: Uploaded file |
| |
| Returns: |
| Privacy analysis results |
| """ |
| try: |
| logger.info(f"π Document privacy analysis: {file.filename}") |
| |
| |
| file_content = await file.read() |
| |
| |
| parse_result = document_parser.parse_document( |
| file_data=file_content, |
| filename=file.filename, |
| mime_type=file.content_type |
| ) |
| |
| if not parse_result["success"]: |
| raise HTTPException( |
| status_code=400, |
| detail=f"Failed to parse document: {parse_result['error']}" |
| ) |
| |
| |
| from services.privacy_detector import analyze_document_privacy |
| privacy_level, privacy_details = analyze_document_privacy( |
| document_text=parse_result["content"], |
| filename=file.filename |
| ) |
| |
| |
| has_sensitive_data = privacy_level.value in ["private", "confidential"] |
| |
| |
| sensitive_types = [] |
| if privacy_details.get("keyword_matches"): |
| sensitive_types.extend([cat for cat, _ in privacy_details["keyword_matches"]]) |
| if privacy_details.get("pattern_matches"): |
| sensitive_types.extend([cat for _, cat in privacy_details["pattern_matches"]]) |
| if privacy_details.get("document_indicators"): |
| sensitive_types.extend(privacy_details["document_indicators"]) |
| sensitive_types = list(set(sensitive_types)) |
| |
| logger.info( |
| f"β
Privacy analysis complete: {file.filename} " |
| f"(sensitive: {has_sensitive_data})" |
| ) |
| |
| return JSONResponse(content={ |
| "success": True, |
| "filename": file.filename, |
| "privacy_level": privacy_level.value, |
| "has_sensitive_data": has_sensitive_data, |
| "sensitive_data_types": sensitive_types, |
| "reason": privacy_details.get("reason", "unknown"), |
| "details": privacy_details |
| }) |
| |
| except HTTPException: |
| raise |
| |
| except Exception as e: |
| logger.error(f"β Document analysis error: {e}", exc_info=True) |
| raise HTTPException( |
| status_code=500, |
| detail=f"Internal server error: {str(e)}" |
| ) |
|
|
|
|
| @router.get("/supported-formats") |
| async def get_supported_formats(): |
| """ |
| Get list of supported document formats |
| |
| Returns: |
| List of supported file formats and their MIME types |
| """ |
| return JSONResponse(content={ |
| "success": True, |
| "supported_formats": [ |
| { |
| "extension": "pdf", |
| "mime_type": "application/pdf", |
| "description": "Adobe PDF Document", |
| "max_size_mb": 10 |
| }, |
| { |
| "extension": "docx", |
| "mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", |
| "description": "Microsoft Word Document (2007+)", |
| "max_size_mb": 10 |
| }, |
| { |
| "extension": "doc", |
| "mime_type": "application/msword", |
| "description": "Microsoft Word Document (Legacy)", |
| "max_size_mb": 10 |
| }, |
| { |
| "extension": "txt", |
| "mime_type": "text/plain", |
| "description": "Plain Text Document", |
| "max_size_mb": 10 |
| } |
| ], |
| "max_file_size_bytes": document_parser.MAX_FILE_SIZE |
| }) |
|
|