import os import uvicorn from fastapi import FastAPI, HTTPException, File, UploadFile from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse, Response from .schemas import ( SingleMatchRequest, SingleMatchResponse, BatchMatchRequest, BatchMatchResponse, LiveJobSearchRequest, SampleDataResponse, AICoachRequest, AICoachResponse, ATSReportRequest ) from .matcher_service import matcher_service from .sample_data import SAMPLE_PERSONAS, SAMPLE_JOBS from .resume_parser import parse_resume_file from .gemini_coach_service import coach_service from .pdf_report_service import generate_ats_audit_pdf app = FastAPI( title="Alture AI — Global Job Intelligence & Explainable ATS Engine", description="Production REST API powering hybrid semantic matching, 500+ skill ontology extraction, and ATS compatibility scoring.", version="2.0.0", docs_url="/docs", redoc_url="/redoc" ) # Enable CORS for local development and microservices app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ---------------------------------------------------- # API ROUTERS # ---------------------------------------------------- @app.get("/health", tags=["Health & System"]) async def health_check(): """Health check endpoint to verify backend operational readiness.""" return { "status": "healthy", "service": "Alture AI Matcher Engine", "version": "2.0.0", "sbert_loaded": matcher_service.sbert_model is not None, "models_loaded": matcher_service.xgb_model is not None or matcher_service.lgb_model is not None } @app.post("/api/v1/upload-resume", tags=["Resume Processing"]) async def upload_resume(file: UploadFile = File(...)): """ Upload and parse candidate resume file (.pdf, .docx, .txt). Extracts text, candidate name, contact details, and word counts. """ try: contents = await file.read() if len(contents) == 0: raise HTTPException(status_code=400, detail="Uploaded file is empty.") parsed_result = parse_resume_file(filename=file.filename, file_bytes=contents) if parsed_result["word_count"] < 10: raise HTTPException(status_code=400, detail="Could not extract readable text from document. Please ensure file is not password-protected or scanned image.") return parsed_result except Exception as e: raise HTTPException(status_code=500, detail=f"Error parsing resume file: {str(e)}") @app.get("/api/v1/sample-data", response_model=SampleDataResponse, tags=["Sample Data"]) async def get_sample_data(): """Retrieve preloaded test candidate personas and global job postings.""" return SampleDataResponse( personas=SAMPLE_PERSONAS, jobs=SAMPLE_JOBS ) @app.post("/api/v1/analyze", response_model=SingleMatchResponse, tags=["ATS Matching"]) async def analyze_single_match(request: SingleMatchRequest): """ Perform deep hybrid NLP analysis between a single candidate resume and job description. Returns calibrated ATS Compatibility Score, Fit Tier, Matched/Missing Skills, and Actionable Feedback. """ try: match_result = matcher_service.analyze_match( resume_text=request.resume_text, jd_text=request.jd_text ) return SingleMatchResponse( status="success", job_title=request.job_title or "Target Position", match_result=match_result ) except Exception as e: raise HTTPException(status_code=500, detail=f"Inference error during matching: {str(e)}") @app.post("/api/v1/match-jobs", response_model=BatchMatchResponse, tags=["Global Job Discovery"]) async def match_against_jobs(request: BatchMatchRequest): """ Match candidate resume against multiple global jobs and return ranked results sorted by compatibility score. """ try: ranked_results = matcher_service.match_against_global_jobs( resume_text=request.resume_text, specific_job_ids=request.job_ids ) return BatchMatchResponse( status="success", total_jobs_evaluated=len(ranked_results), ranked_jobs=ranked_results ) except Exception as e: raise HTTPException(status_code=500, detail=f"Error ranking global jobs: {str(e)}") @app.get("/api/v1/jobs/live", tags=["Live Job Stream"]) async def get_live_jobs(limit: int = 15): """ Fetch real-time live remote tech jobs from the public Remotive API. """ from .live_jobs_service import fetch_live_global_jobs live_jobs = fetch_live_global_jobs(limit=limit) return {"status": "success", "count": len(live_jobs), "jobs": live_jobs} @app.post("/api/v1/search-and-match-jobs", response_model=BatchMatchResponse, tags=["Live Job Stream"]) async def search_and_match_jobs(request: LiveJobSearchRequest): """ Multi-source job search and ATS matching across Pakistan and Worldwide tech feeds. Supports JSearch (LinkedIn, Indeed, Glassdoor) and Remotive. """ from .live_jobs_service import fetch_multi_source_jobs try: jobs, provider_name = fetch_multi_source_jobs( query=request.query or "Software Engineer", location=request.location or "Pakistan", provider=request.provider or "auto", user_api_key=request.rapidapi_key, limit=request.limit or 15 ) ranked_results = matcher_service.match_against_jobs_list( resume_text=request.resume_text, jobs=jobs ) return BatchMatchResponse( status="success", total_jobs_evaluated=len(ranked_results), provider_used=provider_name, search_query=request.query, search_location=request.location, ranked_jobs=ranked_results ) except Exception as e: raise HTTPException(status_code=500, detail=f"Error searching and matching jobs: {str(e)}") @app.post("/api/v1/ai-coach", response_model=AICoachResponse, tags=["AI Career Coach"]) async def ai_career_coach(request: AICoachRequest): """ Gemini-powered AI Career Coach providing: - 'tips': Resume improvement suggestions based on skill gaps - 'cover_letter': Tailored cover letter generation - 'interview_prep': Interview preparation questions """ try: if request.action == "tips": result = coach_service.get_resume_tips( resume_text=request.resume_text, job_title=request.job_title, job_description=request.job_description, matched_skills=request.matched_skills, missing_skills=request.missing_skills, ats_score=request.ats_score ) elif request.action == "cover_letter": result = coach_service.generate_cover_letter( resume_text=request.resume_text, job_title=request.job_title, company=request.company, job_description=request.job_description ) elif request.action == "interview_prep": result = coach_service.generate_interview_questions( job_title=request.job_title, job_description=request.job_description, missing_skills=request.missing_skills, matched_skills=request.matched_skills ) else: raise HTTPException(status_code=400, detail=f"Unknown action: {request.action}. Use 'tips', 'cover_letter', or 'interview_prep'.") return AICoachResponse( status="success", action=request.action, powered_by=result.get("powered_by", "gemini-2.0-flash"), data=result ) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"AI Coach error: {str(e)}") @app.post("/api/v1/download-ats-report", tags=["PDF Reports"]) async def download_ats_audit_report(request: ATSReportRequest): """ Generate and stream an enterprise-grade ATS Audit Report PDF complete with score breakdown, verified skills, critical gaps, and recommendations. """ try: pdf_bytes = generate_ats_audit_pdf( candidate_name=request.candidate_name or "Candidate", job_title=request.job_title or "Target Position", company=request.company or "Tech Company", location=request.location or "Pakistan", ats_score=request.ats_score, fit_tier=request.fit_tier, matched_skills=request.matched_skills or [], missing_skills=request.missing_skills or [], tips=request.tips or [], overall_assessment=request.overall_assessment or "" ) safe_name = "".join(c for c in request.candidate_name if c.isalnum() or c in (' ', '_')).rstrip().replace(' ', '_') filename = f"Alture_AI_ATS_Audit_{safe_name or 'Report'}.pdf" return Response( content=pdf_bytes, media_type="application/pdf", headers={ "Content-Disposition": f'attachment; filename="{filename}"' } ) except Exception as e: raise HTTPException(status_code=500, detail=f"Error generating PDF report: {str(e)}") # ---------------------------------------------------- # SERVE FRONTEND STATIC FILES # ---------------------------------------------------- FRONTEND_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "frontend") if os.path.exists(FRONTEND_DIR): app.mount("/static", StaticFiles(directory=FRONTEND_DIR), name="static") @app.get("/", tags=["Frontend"]) async def serve_frontend(): index_path = os.path.join(FRONTEND_DIR, "index.html") if os.path.exists(index_path): return FileResponse(index_path) return {"message": "Alture AI FastAPI Backend is running. Open /docs for Swagger API."} if __name__ == "__main__": port = int(os.environ.get("PORT", 8000)) print(f"🚀 Starting Alture AI FastAPI Production Server on http://localhost:{port}") uvicorn.run("deployment.backend.main:app", host="0.0.0.0", port=port, reload=True)