Spaces:
Running
Running
| """ | |
| Documents router β upload, analyze, chat, history. | |
| All endpoints require JWT authentication. | |
| """ | |
| import os | |
| from typing import Optional | |
| from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query | |
| from sqlalchemy.orm import Session | |
| from pydantic import BaseModel | |
| from app.database.database import get_db | |
| from app.database.models import User, UploadedDocument, DocumentMessage, generate_uuid | |
| from app.auth.router import get_current_user | |
| from app.documents.service import extract_text, analyze_document, chat_with_document | |
| router = APIRouter(prefix="/api/documents", tags=["Documents"]) | |
| # βββ Config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB | |
| ALLOWED_TYPES = { | |
| "application/pdf": "pdf", | |
| "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx", | |
| "text/plain": "txt", | |
| "text/csv": "csv", | |
| "application/octet-stream": None, # resolved by extension | |
| } | |
| ALLOWED_EXTENSIONS = {"pdf", "docx", "txt", "csv"} | |
| def _resolve_file_type(filename: str, content_type: str) -> str: | |
| ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else "" | |
| if ext in ALLOWED_EXTENSIONS: | |
| return ext | |
| mapped = ALLOWED_TYPES.get(content_type) | |
| if mapped: | |
| return mapped | |
| raise HTTPException(status_code=400, detail=f"Unsupported file type: {content_type}. Allowed: PDF, DOCX, TXT, CSV") | |
| # βββ Upload βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def upload_document( | |
| file: UploadFile = File(...), | |
| language: str = Query(default="en"), | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db), | |
| ): | |
| """Upload a document, extract text, and run AI analysis.""" | |
| file_bytes = await file.read() | |
| if len(file_bytes) > MAX_FILE_SIZE: | |
| raise HTTPException(status_code=413, detail="File too large. Maximum size is 10 MB.") | |
| if not file_bytes: | |
| raise HTTPException(status_code=400, detail="Empty file.") | |
| file_type = _resolve_file_type(file.filename or "upload", file.content_type or "") | |
| # Extract text | |
| extracted_text = extract_text(file_bytes, file_type) | |
| if not extracted_text.strip(): | |
| raise HTTPException( | |
| status_code=422, | |
| detail=( | |
| "No readable text could be extracted from this document. " | |
| "If this is a scanned PDF or image-only statement, please upload a text-based PDF, DOCX, TXT, or CSV file." | |
| ), | |
| ) | |
| # AI analysis | |
| analysis = analyze_document(extracted_text, file.filename or "document", language) | |
| # Persist | |
| doc = UploadedDocument( | |
| id=generate_uuid(), | |
| user_id=current_user.id, | |
| filename=file.filename or "upload", | |
| file_type=file_type, | |
| file_size=len(file_bytes), | |
| extracted_text=extracted_text[:50000], # cap stored text at 50k chars | |
| ai_summary=analysis["summary"], | |
| ai_insights=analysis["insights"] + ( | |
| [f"β οΈ Suspicious: {s}" for s in analysis["suspicious"]] if analysis["suspicious"] else [] | |
| ), | |
| ) | |
| db.add(doc) | |
| db.commit() | |
| db.refresh(doc) | |
| return { | |
| "id": doc.id, | |
| "filename": doc.filename, | |
| "file_type": doc.file_type, | |
| "file_size": doc.file_size, | |
| "extracted_length": len(extracted_text), | |
| "summary": analysis["summary"], | |
| "insights": analysis["insights"], | |
| "suspicious": analysis["suspicious"], | |
| "created_at": doc.created_at.isoformat() if doc.created_at else None, | |
| } | |
| # βββ Re-analyze βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def analyze_existing( | |
| doc_id: str, | |
| language: str = Query(default="en"), | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db), | |
| ): | |
| doc = db.query(UploadedDocument).filter( | |
| UploadedDocument.id == doc_id, | |
| UploadedDocument.user_id == current_user.id, | |
| ).first() | |
| if not doc: | |
| raise HTTPException(status_code=404, detail="Document not found.") | |
| analysis = analyze_document(doc.extracted_text or "", doc.filename, language) | |
| doc.ai_summary = analysis["summary"] | |
| doc.ai_insights = analysis["insights"] + [f"β οΈ {s}" for s in analysis["suspicious"]] | |
| db.commit() | |
| return { | |
| "id": doc.id, | |
| "summary": analysis["summary"], | |
| "insights": analysis["insights"], | |
| "suspicious": analysis["suspicious"], | |
| } | |
| # βββ Chat with document βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class DocChatRequest(BaseModel): | |
| question: str | |
| language: str = "en" | |
| def chat_document( | |
| doc_id: str, | |
| req: DocChatRequest, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db), | |
| ): | |
| doc = db.query(UploadedDocument).filter( | |
| UploadedDocument.id == doc_id, | |
| UploadedDocument.user_id == current_user.id, | |
| ).first() | |
| if not doc: | |
| raise HTTPException(status_code=404, detail="Document not found.") | |
| # Load conversation history for this document | |
| history_msgs = ( | |
| db.query(DocumentMessage) | |
| .filter( | |
| DocumentMessage.document_id == doc_id, | |
| DocumentMessage.user_id == current_user.id, | |
| ) | |
| .order_by(DocumentMessage.created_at.asc()) | |
| .limit(20) | |
| .all() | |
| ) | |
| history = [{"role": m.role, "content": m.content} for m in history_msgs] | |
| # Get AI response | |
| answer = chat_with_document( | |
| question=req.question, | |
| extracted_text=doc.extracted_text or "", | |
| filename=doc.filename, | |
| history=history, | |
| language=req.language, | |
| ) | |
| # Persist both messages | |
| user_msg = DocumentMessage( | |
| id=generate_uuid(), | |
| user_id=current_user.id, | |
| document_id=doc_id, | |
| role="user", | |
| content=req.question, | |
| language=req.language, | |
| ) | |
| ai_msg = DocumentMessage( | |
| id=generate_uuid(), | |
| user_id=current_user.id, | |
| document_id=doc_id, | |
| role="assistant", | |
| content=answer, | |
| language=req.language, | |
| ) | |
| db.add(user_msg) | |
| db.add(ai_msg) | |
| db.commit() | |
| return { | |
| "question": req.question, | |
| "answer": answer, | |
| "document_id": doc_id, | |
| "language": req.language, | |
| } | |
| # βββ History ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_document_history( | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db), | |
| ): | |
| docs = ( | |
| db.query(UploadedDocument) | |
| .filter(UploadedDocument.user_id == current_user.id) | |
| .order_by(UploadedDocument.created_at.desc()) | |
| .limit(20) | |
| .all() | |
| ) | |
| return { | |
| "documents": [ | |
| { | |
| "id": d.id, | |
| "filename": d.filename, | |
| "file_type": d.file_type, | |
| "file_size": d.file_size, | |
| "summary": d.ai_summary, | |
| "insights": d.ai_insights or [], | |
| "extracted_length": len(d.extracted_text or ""), | |
| "created_at": d.created_at.isoformat() if d.created_at else None, | |
| } | |
| for d in docs | |
| ] | |
| } | |
| # βββ Single document + its chat βββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_document( | |
| doc_id: str, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db), | |
| ): | |
| doc = db.query(UploadedDocument).filter( | |
| UploadedDocument.id == doc_id, | |
| UploadedDocument.user_id == current_user.id, | |
| ).first() | |
| if not doc: | |
| raise HTTPException(status_code=404, detail="Document not found.") | |
| messages = ( | |
| db.query(DocumentMessage) | |
| .filter( | |
| DocumentMessage.document_id == doc_id, | |
| DocumentMessage.user_id == current_user.id, | |
| ) | |
| .order_by(DocumentMessage.created_at.asc()) | |
| .all() | |
| ) | |
| return { | |
| "id": doc.id, | |
| "filename": doc.filename, | |
| "file_type": doc.file_type, | |
| "file_size": doc.file_size, | |
| "summary": doc.ai_summary, | |
| "insights": doc.ai_insights or [], | |
| "extracted_length": len(doc.extracted_text or ""), | |
| "created_at": doc.created_at.isoformat() if doc.created_at else None, | |
| "messages": [ | |
| { | |
| "id": m.id, | |
| "role": m.role, | |
| "content": m.content, | |
| "language": m.language, | |
| "created_at": m.created_at.isoformat() if m.created_at else None, | |
| } | |
| for m in messages | |
| ], | |
| } | |
| # βββ Delete βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def delete_document( | |
| doc_id: str, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db), | |
| ): | |
| doc = db.query(UploadedDocument).filter( | |
| UploadedDocument.id == doc_id, | |
| UploadedDocument.user_id == current_user.id, | |
| ).first() | |
| if not doc: | |
| raise HTTPException(status_code=404, detail="Document not found.") | |
| db.delete(doc) | |
| db.commit() | |
| return {"message": "Document deleted."} | |