docsift-backend-host/docsift-api
0
1import os2import jwt3import json # تم إضافة الـ Import الناقص4import asyncio5import uuid6import smtplib7import random8import traceback9import easyocr10import hashlib11import hmac12import numpy as np13from PIL import Image14from fastapi import FastAPI, UploadFile, File, HTTPException, BackgroundTasks, Depends, Request, Form, Security, APIRouter, Header15from fastapi.middleware.cors import CORSMiddleware16from fastapi.security.api_key import APIKeyHeader17from email.mime.text import MIMEText18from typing import Optional19from email.mime.multipart import MIMEMultipart20from fastapi.responses import FileResponse, JSONResponse 21from datetime import datetime, timedelta22from dotenv import load_dotenv23from supabase import create_client, Client24from slowapi import Limiter, _rate_limit_exceeded_handler25from slowapi.util import get_remote_address26from slowapi.errors import RateLimitExceeded27from starlette.requests import Request28from starlette.status import HTTP_403_FORBIDDEN29 30# استيراد الدوال الـ Async والـ Processor المطور31from database import (32 save_file_info, 33 save_analysis_results, 34 get_dashboard_data, 35 get_files_from_db, 36 get_file_by_id, 37 delete_file_db,38 run_sync_in_async,39 supabase40)41from storage import upload_file_to_r2, delete_from_r2, R2_PUBLIC_URL42from processor import DocumentProcessor 43from pydantic import BaseModel, EmailStr44 45# مكتبات الـ PDF للتقارير46from reportlab.lib import colors47from reportlab.lib.units import inch48from reportlab.lib.pagesizes import letter49from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image, HRFlowable50from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle51from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT52 53load_dotenv()54 55app = FastAPI(title="DocSift AI - High Scale Edition")56limiter = Limiter(key_func=get_remote_address)57app.state.limiter = limiter58app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)59 60# نظام "العساس" (Semaphore) لضمان عدم انهيار السيرفر المجاني61analysis_semaphore = asyncio.Semaphore(2)62API_KEY_NAME = "X-API-Key"63api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)64 65# إعداد الـ CORS66app.add_middleware(67 CORSMiddleware,68 allow_origins=["*"],69 allow_credentials=True,70 allow_methods=["GET", "POST", "DELETE", "OPTIONS", "PUT"],71 allow_headers=["*"],72)73 74class UserSignUp(BaseModel):75 full_name: str76 email: EmailStr77 password: str78 79class UserLogin(BaseModel):80 email: EmailStr81 password: str82 83class VerifyOTP(BaseModel):84 email: str85 token: str86 87token = os.getenv("SUPABASE_KEY")88if token:89 try:90 decoded = jwt.decode(token, options={"verify_signature": False})91 print(f"[CHECK] Current Key Role: {decoded.get('role')}")92 except Exception:93 print("[CHECK] Cannot decode SUPABASE_KEY")94 95url = os.getenv("SUPABASE_URL")96supabase: Client = create_client(url, token)97 98PADDLE_WEBHOOK_SECRET = os.getenv("PADDLE_WEBHOOK_SECRET")99router = APIRouter(prefix="/webhooks", tags=["webhooks"])100 101@app.get("/")102async def read_root():103 return {"status": "online", "message": "Neural Engine OCR-Ready & Scalable"}104 105def send_auth_email(target_email: str, code: str):106 sender_email = os.getenv("GMAIL_USER")107 app_password = os.getenv("GMAIL_PASSWORD")108 109 message = MIMEMultipart()110 message["From"] = f"DocSift Official <{sender_email}>"111 message["To"] = target_email112 message["Subject"] = f"{code} is your DocSift verification code"113 114 html = f"""115 <div style="font-family: sans-serif; max-width: 400px; margin: auto; border: 1px solid #eee; padding: 20px; border-radius: 10px;">116 <h2 style="color: #333; text-align: center;">Verify your account</h2>117 <p style="color: #555;">Use the code below to access your DocSift dashboard. This code is valid for <b>5 minutes</b>.</p>118 <div style="background: #f4f4f4; font-size: 32px; font-weight: bold; text-align: center; padding: 15px; color: #4f46e5; letter-spacing: 5px; border-radius: 5px;">119 {code}120 </div>121 <p style="font-size: 12px; color: #888; margin-top: 20px; text-align: center;">If you didn't request this code, please ignore this email.</p>122 </div>123 """124 message.attach(MIMEText(html, "html"))125 126 try:127 with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:128 server.login(sender_email, app_password)129 server.sendmail(sender_email, target_email, message.as_string())130 print(f"✅ Email sent to {target_email}")131 except Exception as e:132 print(f"❌ SMTP Error: {str(e)}")133 134@app.post("/auth/signup")135@limiter.limit("5/hour")136async def signup(request: Request, user: UserSignUp, background_tasks: BackgroundTasks):137 try:138 auth_res = await run_sync_in_async(lambda: supabase.auth.sign_up({139 "email": user.email, "password": user.password140 }))141 142 otp_code = str(random.randint(100000, 999999))143 expiry_time = datetime.utcnow() + timedelta(minutes=5)144 145 await run_sync_in_async(lambda: supabase.table("otps").upsert({146 "email": user.email,147 "code": otp_code,148 "expires_at": expiry_time.isoformat()149 }, on_conflict="email").execute())150 151 background_tasks.add_task(send_auth_email, user.email, otp_code)152 return {"status": "success", "message": "Verification code sent!"}153 except Exception as e:154 print(f"🔥 Signup Error: {str(e)}")155 raise HTTPException(status_code=400, detail="Account creation failed.")156 157@app.post("/auth/verify")158async def verify_email(data: VerifyOTP):159 try:160 otp_res = await run_sync_in_async(lambda: supabase.table("otps").select("*").eq("email", data.email).single().execute())161 if not otp_res.data or otp_res.data['code'] != data.token:162 raise HTTPException(status_code=400, detail="Invalid verification code.")163 164 profile_res = await run_sync_in_async(lambda: supabase.table("profiles").select("id").eq("email", data.email).execute())165 if not profile_res.data:166 raise HTTPException(status_code=404, detail="Profile not found.")167 168 user_id = profile_res.data[0]['id']169 await run_sync_in_async(lambda: supabase.table("profiles").update({"is_verified": True, "credits": 10}).eq("id", user_id).execute())170 await run_sync_in_async(lambda: supabase.table("otps").delete().eq("email", data.email).execute())171 172 return {"status": "success", "message": "Verified!"}173 except Exception as e:174 raise HTTPException(status_code=400, detail="Verification failed.")175 176@app.post("/auth/login")177async def login(user: UserLogin):178 try:179 res = await run_sync_in_async(lambda: supabase.auth.sign_in_with_password({180 "email": user.email, "password": user.password181 }))182 183 profile = await run_sync_in_async(lambda: supabase.table("profiles").select("*").eq("email", user.email).single().execute())184 if not profile.data or not profile.data.get("is_verified"):185 raise HTTPException(status_code=401, detail="Please verify your email first!")186 187 return {188 "token": res.session.access_token,189 "user_id": res.user.id,190 "full_name": profile.data.get("full_name", ""),191 "credits": profile.data.get("credits", 0)192 }193 except Exception as e:194 raise HTTPException(status_code=401, detail="Invalid credentials.")195 196@app.post("/auth/forgot-password")197@limiter.limit("3/minute")198async def forgot_password(request: Request, data: dict, background_tasks: BackgroundTasks):199 email = data.get("email")200 if not email:201 raise HTTPException(status_code=400, detail="Email is required")202 203 otp = str(random.randint(100000, 999999))204 try:205 await run_sync_in_async(lambda: supabase.table("otps").upsert({206 "email": email, "code": otp,207 "expires_at": (datetime.utcnow() + timedelta(minutes=10)).isoformat()208 }, on_conflict="email").execute())209 210 background_tasks.add_task(send_auth_email, email, otp)211 return {"message": "Reset code sent successfully"}212 except Exception as e:213 raise HTTPException(status_code=500, detail="Could not process request")214 215@app.post("/auth/reset-password")216async def reset_password(data: dict):217 email, token, new_password = data.get("email"), data.get("token"), data.get("new_password")218 if not all([email, token, new_password]):219 raise HTTPException(status_code=400, detail="Missing fields")220 221 try:222 otp_res = await run_sync_in_async(lambda: supabase.table("otps").select("*").eq("email", email).single().execute())223 if not otp_res.data or otp_res.data['code'] != token:224 raise HTTPException(status_code=400, detail="Invalid code")225 226 user_list = await run_sync_in_async(lambda: supabase.auth.admin.list_users())227 target_user = next((u for u in user_list if u.email == email), None)228 229 if not target_user:230 raise HTTPException(status_code=404, detail="User not found")231 232 await run_sync_in_async(lambda: supabase.auth.admin.update_user_by_id(target_user.id, attributes={"password": new_password}))233 await run_sync_in_async(lambda: supabase.table("otps").delete().eq("email", email).execute())234 235 return {"status": "success", "message": "Password updated"}236 except Exception as e:237 raise HTTPException(status_code=500, detail="Reset failed")238 239async def process_and_analyze_task(file_id: str, filename: str, content: bytes, file_url: str):240 try:241 print(f"⚙️ Background Work Starting: {filename}")242 text = await DocumentProcessor.extract_text(content, filename.split('.')[-1])243 analysis = await DocumentProcessor.analyze_risk(text)244 245 update_data = {246 "risk_score": int(analysis.get("risk_score", 0)), 247 "compliance_score": int(analysis.get("compliance_score", 0)), 248 "status": "complete",249 "breakdown": analysis.get("breakdown", {}) 250 }251 252 print(f"📊 Sending Data to DB: {update_data}")253 await run_sync_in_async(lambda: supabase.table("files").update(update_data).eq("id", file_id).execute())254 await save_analysis_results(file_id, analysis)255 print(f"✅ Analysis finished for {filename}")256 except Exception as e:257 print(f"❌ Worker Error: {str(e)}")258 try:259 await run_sync_in_async(lambda: supabase.table("files").update({"status": "error"}).eq("id", file_id).execute())260 except:261 pass262 263@app.post('/upload')264async def upload(265 background_tasks: BackgroundTasks, 266 user_id: str = Form(...), 267 file: UploadFile = File(...)268):269 try:270 user_res = await run_sync_in_async(lambda: supabase.table("profiles").select("credits").eq("id", user_id).single().execute())271 current_credits = user_res.data.get("credits", 0) if user_res.data else 0272 273 if current_credits <= 0:274 raise HTTPException(status_code=403, detail="Out of credits.")275 276 file_content = await file.read()277 file_id = str(uuid.uuid4())278 r2_path = f"{user_id}/{file.filename}"279 280 upload_success = await upload_file_to_r2(file_content, r2_path)281 if not upload_success:282 raise HTTPException(status_code=500, detail="Storage error")283 284 await run_sync_in_async(lambda: supabase.table("files").insert({285 "id": file_id, "user_id": user_id, "name": file.filename,286 "url": f"{R2_PUBLIC_URL}/{r2_path}", "status": "processing"287 }).execute())288 289 await run_sync_in_async(lambda: supabase.table("profiles").update({"credits": current_credits - 1}).eq("id", user_id).execute())290 background_tasks.add_task(queued_analysis, file_id, file.filename, file_content, r2_path)291 292 return {"status": "processing", "file_id": file_id, "remaining_credits": current_credits - 1}293 except Exception as e:294 print(f"❌ Upload Error: {e}")295 raise HTTPException(status_code=500, detail=str(e))296 297async def queued_analysis(file_id, filename, content, r2_path):298 async with analysis_semaphore: 299 print(f"🚀 Semaphore granted for: {filename}. Starting analysis...")300 await process_and_analyze_task(file_id, filename, content, r2_path)301 302@app.get("/dashboard-stats")303async def get_dashboard(user_id: str): 304 try:305 profile_res = await run_sync_in_async(lambda: supabase.table("profiles").select("credits").eq("id", user_id).single().execute())306 credits = profile_res.data.get("credits", 0) if profile_res.data else 0307 308 files_res = await run_sync_in_async(lambda: supabase.table("files").select("*").eq("user_id", user_id).order("created_at", desc=True).execute())309 files = files_res.data or []310 311 total_docs = len(files)312 total_risks = sum(1 for f in files if f.get("risk_score", 0) > 50)313 314 compliance_scores = [f.get("compliance_score", 0) for f in files]315 compliance_rate = int(sum(compliance_scores) / total_docs) if total_docs > 0 else 100316 317 return {318 "total_docs": total_docs,319 "total_risks": total_risks,320 "compliance_rate": compliance_rate,321 "credits": credits, 322 "recent_activity": files[:5] 323 }324 except Exception as e:325 print(f"❌ Dashboard Error: {str(e)}")326 raise HTTPException(status_code=500, detail="Failed to fetch dashboard intelligence.")327 328# 🔥 تصحيح الـ الـ Vault: جلب ملفات المستخدم الحالي فقط بناءً على الـ user_id329@app.get("/files")330async def get_all_files(user_id: str):331 if not user_id:332 raise HTTPException(status_code=400, detail="Missing user_id parameter")333 res = await run_sync_in_async(lambda: supabase.table("files").select("*").eq("user_id", user_id).order("created_at", desc=True).execute())334 return res.data335 336@app.api_route("/files/{file_id}", methods=["DELETE", "OPTIONS"])337async def delete_file(file_id: str, request: Request):338 user_id = request.query_params.get("user_id")339 print(f"--- PURGE ATTEMPT --- Target ID: {file_id} | User ID: {user_id}")340 341 file_data = await get_file_by_id(file_id)342 if not file_data:343 raise HTTPException(status_code=404, detail="Asset not found in database")344 345 if str(file_data.get('user_id')) != user_id:346 raise HTTPException(status_code=403, detail="Unauthorized purge request")347 348 await asyncio.gather(349 delete_from_r2(file_data.get('name') or file_data.get('storage_path')),350 delete_file_db(file_id)351 )352 return {"status": "purged"}353 354@app.get("/generate-report/{file_id}")355async def generate_report(file_id: str):356 file_data = await get_file_by_id(file_id)357 if not file_data:358 raise HTTPException(status_code=404, detail="File not found")359 360 file_path = f"generated_reports/Audit_{file_id}.pdf"361 os.makedirs("generated_reports", exist_ok=True)362 report_hash = hashlib.sha256(f"{file_id}-{datetime.now()}".encode()).hexdigest()[:16].upper()363 364 doc = SimpleDocTemplate(file_path, pagesize=letter, rightMargin=40, leftMargin=40, topMargin=30, bottomMargin=30)365 styles = getSampleStyleSheet()366 elements = []367 368 def add_watermark(canvas, doc):369 canvas.saveState()370 canvas.setFont('Helvetica-Bold', 60)371 canvas.setStrokeColor(colors.lightgrey)372 canvas.setFillAlpha(0.1) 373 canvas.translate(300, 400)374 canvas.rotate(45)375 canvas.drawCentredString(0, 0, "OFFICIAL DOCSIFT AUDIT")376 canvas.rotate(-45)377 canvas.setFont('Helvetica', 7)378 canvas.setFillAlpha(0.5)379 canvas.drawString(-250, -380, f"AUTHENTICITY HASH: {report_hash} | VERIFY AT DOCSIFT.AI/VERIFY")380 canvas.restoreState()381 382 header_style = ParagraphStyle('MainTitle', fontSize=22, fontName='Helvetica-Bold', textColor=colors.HexColor("#1E1B4B"))383 legal_warning_style = ParagraphStyle('Warning', fontSize=7, textColor=colors.red, alignment=TA_CENTER, leading=8)384 385 logo_path = os.path.join("assets", "logo.png")386 logo_img = "DOCSIFT AI"387 388 if os.path.exists(logo_path):389 try:390 logo_img = Image(logo_path)391 desired_width = 1.2 * inch392 aspect = logo_img.imageHeight / float(logo_img.imageWidth)393 logo_img.drawWidth = desired_width394 logo_img.drawHeight = desired_width * aspect395 logo_img.hAlign = 'LEFT'396 except Exception as e:397 print(f"❌ Logo Load Error: {e}")398 logo_img = "DOCSIFT AI"399 400 header_data = [[logo_img, Paragraph("NEURAL AUDIT REPORT", header_style)]]401 header_table = Table(header_data, colWidths=[100, 420])402 header_table.setStyle(TableStyle([('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), ('ALIGN', (1, 0), (1, 0), 'RIGHT')]))403 elements.append(header_table)404 elements.append(HRFlowable(width="100%", thickness=2, color=colors.HexColor("#4F46E5"), spaceAfter=20))405 406 warning_text = "PROTECTED DOCUMENT: This audit is cryptographically linked to DocSift Neural Systems. Unauthorized alteration is a federal offense."407 elements.append(Paragraph(warning_text, legal_warning_style))408 elements.append(Spacer(1, 15))409 410 meta_data = [411 ["ASSET NAME:", file_data['name']],412 ["AUDIT ID:", f"DOC-{file_id[:8].upper()}"],413 ["TIMESTAMP:", datetime.now().strftime('%Y-%m-%d %H:%M:%S')],414 ["SECURITY HASH:", report_hash] 415 ]416 meta_table = Table(meta_data, colWidths=[120, 380])417 meta_table.setStyle(TableStyle([('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'), ('TEXTCOLOR', (0,0), (0,-1), colors.HexColor("#374151"))]))418 elements.append(meta_table)419 elements.append(Spacer(1, 20))420 421 data = [422 ["AUDIT CATEGORY", "SCORE", "RISK ASSESSMENT"],423 ["Legal Exposure", f"{int(file_data.get('breakdown', {}).get('legal', 0))}%", "VERIFIED"],424 ["Financial Liability", f"{int(file_data.get('breakdown', {}).get('financial', 0))}%", "SECURE"],425 ["Compliance Overall", f"{int(file_data.get('compliance_score', 0))}%", "CERTIFIED"],426 ]427 analysis_table = Table(data, colWidths=[200, 100, 200])428 analysis_table.setStyle(TableStyle([429 ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#1E1B4B")),430 ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),431 ('ALIGN', (0, 0), (-1, -1), 'CENTER'),432 ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),433 ('GRID', (0, 0), (-1, -1), 0.5, colors.grey),434 ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor("#F3F4F6")]),435 ]))436 elements.append(analysis_table)437 438 elements.append(Spacer(1, 80))439 elements.append(HRFlowable(width="100%", thickness=0.5, color=colors.black))440 441 strict_legal = f"""442 <b>CRITICAL LEGAL NOTICE:</b> This document is issued under the Digital Assets Security Act. Any attempt to modify, forge, or misrepresent the scores herein is strictly prohibited and punishable under international fraud and cybercrime laws. 443 This report is verified by Hash <b>{report_hash}</b>. Forgery will be detected upon verification against DocSift's central database. 444 DocSift AI assumes no liability for external use. All rights reserved © {datetime.now().year}.445 """446 elements.append(Paragraph(strict_legal, ParagraphStyle('Strict', fontSize=7, leading=9, textColor=colors.HexColor("#111827"))))447 448 doc.build(elements, onFirstPage=add_watermark, onLaterPages=add_watermark)449 return FileResponse(path=file_path, filename=f"SECURE_AUDIT_{file_data['name']}.pdf")450 451@app.get("/api-keys")452async def get_keys():453 res = await run_sync_in_async(lambda: supabase.table("api_keys").select("*").execute())454 return res.data455 456@app.post("/api-keys")457async def create_key(data: dict):458 new_key = f"ds_{uuid.uuid4().hex}"459 key_data = {"name": data['name'], "key_value": new_key}460 res = await run_sync_in_async(lambda: supabase.table("api_keys").insert(key_data).execute())461 return res.data[0]462 463# تعديل وتأمين الـ DELETE المكرر ليعمل بشكل سليم بـ الـ query params464@app.delete("/files/{file_id}")465async def delete_file_route(file_id: str, user_id: str):466 try:467 file_info = await get_file_by_id(file_id)468 if not file_info:469 raise HTTPException(status_code=404, detail="File not found")470 471 if str(file_info.get('user_id')) != user_id:472 raise HTTPException(status_code=403, detail="Unauthorized purge request")473 474 file_name_in_r2 = file_info.get('storage_path') or file_info.get('name')475 await delete_from_r2(file_name_in_r2)476 await delete_file_db(file_id)477 478 return {"message": "File deleted successfully"}479 except Exception as e:480 print(f"❌ DELETE ERROR: {e}")481 raise HTTPException(status_code=500, detail=str(e))482 483def verify_paddle_signature(signature: str, body: bytes) -> bool:484 try:485 if not signature or not PADDLE_WEBHOOK_SECRET:486 return False487 parts = dict(item.split('=') for item in signature.split(';'))488 ts = parts.get('ts')489 h = parts.get('h')490 if not ts or not h:491 return False492 signed_payload = f"{ts}:{body.decode('utf-8')}"493 computed_hash = hmac.new(494 PADDLE_WEBHOOK_SECRET.encode('utf-8'),495 signed_payload.encode('utf-8'),496 hashlib.sha256497 ).hexdigest()498 return hmac.compare_digest(computed_hash, h)499 except Exception:500 return False501 502async def fulfill_order(payload: dict):503 try:504 event_data = payload.get("data", {})505 custom_data = event_data.get("custom_data", {})506 user_id = custom_data.get("user_id")507 plan_type = custom_data.get("plan_type")508 509 if not user_id:510 return511 512 credits_map = {"pro": 200, "enterprise": 1500}513 amount = credits_map.get(plan_type, 0)514 515 if amount > 0:516 profile = await run_sync_in_async(lambda: supabase.table("profiles").select("credits").eq("id", user_id).single().execute())517 new_total = (profile.data.get("credits", 0) if profile.data else 0) + amount518 await run_sync_in_async(lambda: supabase.table("profiles").update({"credits": new_total, "plan": plan_type}).eq("id", user_id).execute())519 print(f"✅ User {user_id} upgraded! +{amount} credits added.")520 except Exception as e:521 print(f"❌ Fulfillment Error: {str(e)}")522 523@app.post("/webhooks/paddle")524async def paddle_webhook(525 request: Request, 526 background_tasks: BackgroundTasks,527 paddle_signature: Optional[str] = Header(None)528):529 body = await request.body()530 if not verify_paddle_signature(paddle_signature, body):531 raise HTTPException(status_code=401, detail="Invalid Signature")532 533 payload = json.loads(body)534 event_type = payload.get("event_type")535 536 if event_type == "transaction.completed":537 background_tasks.add_task(fulfill_order, payload)538 return {"status": "processing"}539 540 return {"status": "ignored"}541 542@app.middleware("http")543async def global_middleware(request: Request, call_next):544 path = request.url.path545 if path.startswith("/files/") and path.endswith("/"):546 request.scope['path'] = path[:-1]547 try:548 response = await call_next(request)549 if response.status_code == 404:550 print(f"🔍 404 ALERT: Route not found -> {request.method} {request.url.path}")551 return response552 except Exception as e:553 print("\n" + "🔥"*20 + "\nCRITICAL ERROR CAUGHT:\n" + traceback.format_exc() + "\n" + "🔥"*20 + "\n")554 return JSONResponse(555 status_code=500,556 content={"detail": "Neural Link Failure. Our engineers are on it!"}557 )558 559async def get_api_key(api_key_header: str = Security(api_key_header)):560 if not api_key_header:561 raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="API Key is missing. Check 'X-API-Key' header.")562 res = await run_sync_in_async(lambda: supabase.table("api_keys").select("*").eq("key_value", api_key_header).execute())563 if not res.data:564 raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Invalid or Revoked API Key.") # تم تصحيح الـ Typo هنا565 return res.data[0]566 567@app.post("/v1/analyze")568async def b2b_analyze(569 background_tasks: BackgroundTasks,570 file: UploadFile = File(...),571 key_info: dict = Depends(get_api_key)572):573 try:574 user_id = key_info.get("user_id")575 file_id = str(uuid.uuid4())576 content = await file.read()577 r2_path = f"uploads/{user_id}/{file_id}_{file.filename}"578 background_tasks.add_task(process_and_analyze_task, file_id, file.filename, content, r2_path)579 return {580 "status": "processing",581 "file_id": file_id,582 "message": "Document received. Analysis started in background.",583 "request_id": str(uuid.uuid4())[:8]584 }585 except Exception as e:586 raise HTTPException(status_code=500, detail=f"Server Error: {str(e)}")