from typing import List from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from sqlalchemy import and_ from app.core.deps import get_db, get_current_active_user from app.core.cache import match_cache, cache_response from app.models.user import User from app.models.resume import Resume from app.models.job import Job from app.models.match import Match, SkillGap from app.schemas.match import MatchResponse, MatchRequest from app.services.ai_service import AIService from app.models.analytics import Analytics router = APIRouter() ai_service = AIService() @router.post("/analyze", response_model=MatchResponse) async def analyze_match( match_request: MatchRequest, current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db) ): """Analyze match between resume and job description with caching""" # Check cache first cache_key = f"match_{current_user.id}_{match_request.resume_id}_{match_request.job_id}" cached_match = match_cache.get(cache_key) if cached_match: return cached_match # Check if match already exists in database existing_match = db.query(Match).filter(and_( Match.user_id == current_user.id, Match.resume_id == match_request.resume_id, Match.job_id == match_request.job_id )).first() if existing_match: # Cache the existing match match_cache.set(cache_key, existing_match, ttl=1800) # 30 minutes return existing_match # Optimized single query to get both resume and job resume = db.query(Resume).filter(and_( Resume.id == match_request.resume_id, Resume.user_id == current_user.id )).first() if not resume: raise HTTPException(status_code=404, detail="Resume not found") job = db.query(Job).filter(and_( Job.id == match_request.job_id, Job.is_active )).first() if not job: raise HTTPException(status_code=404, detail="Job not found") # Prepare data for AI analysis resume_data = { "skills": resume.skills or [], "experience_years": resume.experience_years, "education_level": resume.education_level, "parsed_data": resume.parsed_data or {} } job_data = { "required_skills": job.required_skills or [], "preferred_skills": job.preferred_skills or [], "experience_level": job.experience_level, "education_requirement": job.education_requirement, "description": job.description } # Calculate match score and suggestions in parallel using AI import asyncio try: # Run AI operations concurrently for better performance match_analysis_task = ai_service.calculate_match_score(resume_data, job_data) suggestions_task = None # Get match analysis first match_analysis = await match_analysis_task # Then get suggestions based on analysis if match_analysis: suggestions = await ai_service.generate_resume_suggestions( resume_data, job_data, match_analysis ) else: suggestions = [] # Create match record match = Match( user_id=current_user.id, resume_id=match_request.resume_id, job_id=match_request.job_id, match_score=match_analysis.get("overall_score", 0), skill_match_score=match_analysis.get("skill_match_score", 0), experience_match_score=match_analysis.get("experience_match_score", 0), education_match_score=match_analysis.get("education_match_score", 0), overall_feedback=match_analysis.get("overall_feedback", ""), resume_suggestions=suggestions ) # Batch database operations for better performance db.add(match) db.flush() # Get the match ID without committing # Create skill gap records in batch missing_skills = match_analysis.get("missing_skills", []) skill_gaps = [] for skill_data in missing_skills: skill_gap = SkillGap( match_id=match.id, missing_skill=skill_data.get("skill", ""), importance=skill_data.get("importance", ""), suggestion=skill_data.get("suggestion", "") ) skill_gaps.append(skill_gap) if skill_gaps: db.add_all(skill_gaps) # Add analytics analytics = Analytics( user_id=current_user.id, event_type="job_match", event_data={ "resume_id": match_request.resume_id, "job_id": match_request.job_id, "match_score": match.match_score }, improvement_score=match.match_score ) db.add(analytics) # Single commit for all operations db.commit() db.refresh(match) # Cache the result match_cache.set(cache_key, match, ttl=1800) # 30 minutes return match except Exception as e: db.rollback() raise HTTPException(status_code=500, detail=f"Error processing match: {str(e)}") @router.get("/", response_model=List[MatchResponse]) def get_matches( current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db) ): """Get all matches for current user""" return db.query(Match).filter(Match.user_id == current_user.id).all() @router.get("/{match_id}", response_model=MatchResponse) def get_match( match_id: int, current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db) ): """Get specific match""" match = db.query(Match).filter( Match.id == match_id, Match.user_id == current_user.id ).first() if not match: raise HTTPException(status_code=404, detail="Match not found") return match @router.post("/{match_id}/cover-letter") async def generate_cover_letter( match_id: int, current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db) ): """Generate cover letter for specific match""" match = db.query(Match).filter( Match.id == match_id, Match.user_id == current_user.id ).first() if not match: raise HTTPException(status_code=404, detail="Match not found") # Get resume and job data resume = db.query(Resume).filter(Resume.id == match.resume_id).first() job = db.query(Job).filter(Job.id == match.job_id).first() resume_data = { "skills": resume.skills or [], "experience_years": resume.experience_years, "education_level": resume.education_level, "parsed_data": resume.parsed_data or {} } job_data = { "title": job.title, "company": job.company, "description": job.description, "required_skills": job.required_skills or [], "preferred_skills": job.preferred_skills or [] } # Generate cover letter cover_letter = await ai_service.generate_cover_letter( resume_data, job_data, current_user.full_name ) # Update match with cover letter match.cover_letter = cover_letter db.commit() # Log analytics analytics = Analytics( user_id=current_user.id, event_type="cover_letter_generate", event_data={ "match_id": match_id, "job_id": match.job_id } ) db.add(analytics) db.commit() return {"cover_letter": cover_letter}