Bankbot / backend /app /documents /router.py
mohsin-devs's picture
Fix document PDF text extraction
1f11d61
Raw
History Blame Contribute Delete
10.2 kB
"""
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 ───────────────────────────────────────────────────────────────────
@router.post("/upload", status_code=201)
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 ───────────────────────────────────────────────────────────────
@router.post("/analyze/{doc_id}")
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"
@router.post("/chat/{doc_id}")
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 ──────────────────────────────────────────────────────────────────
@router.get("/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 ───────────────────────────────────────────────
@router.get("/{doc_id}")
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 ───────────────────────────────────────────────────────────────────
@router.delete("/{doc_id}")
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."}