from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from fastapi.responses import RedirectResponse from pydantic import BaseModel from app.core.database import get_session from app.repositories.resume_repository import ResumeRepository from celery_worker.interview_analysis_task import ( analyze_multiple_candidates, generate_interview_report, ) router = APIRouter(prefix="/analysis", tags=["analysis"]) class AnalysisResponse(BaseModel): """Ответ запуска задачи анализа""" message: str resume_id: int task_id: str class BulkAnalysisRequest(BaseModel): """Запрос массового анализа""" resume_ids: list[int] class BulkAnalysisResponse(BaseModel): """Ответ массового анализа""" message: str resume_count: int task_id: str class CandidateRanking(BaseModel): """Рейтинг кандидата""" resume_id: int candidate_name: str overall_score: int recommendation: str position: str @router.post("/interview-report/{resume_id}", response_model=AnalysisResponse) async def start_interview_analysis( resume_id: int, background_tasks: BackgroundTasks, resume_repo: ResumeRepository = Depends(ResumeRepository), ): """ Запускает анализ интервью для конкретного кандидата Анализирует: - Соответствие резюме вакансии - Качество ответов в диалоге интервью - Технические навыки и опыт - Коммуникативные способности - Общую рекомендацию и рейтинг """ # Проверяем, существует ли резюме resume = await resume_repo.get_by_id(resume_id) if not resume: raise HTTPException(status_code=404, detail="Resume not found") # Запускаем задачу анализа task = generate_interview_report.delay(resume_id) return AnalysisResponse( message="Interview analysis started", resume_id=resume_id, task_id=task.id ) @router.post("/bulk-analysis", response_model=BulkAnalysisResponse) async def start_bulk_analysis( request: BulkAnalysisRequest, background_tasks: BackgroundTasks, resume_repo: ResumeRepository = Depends(ResumeRepository), ): """ Запускает массовый анализ нескольких кандидатов Возвращает ранжированный список кандидатов по общему баллу Полезно для сравнения кандидатов на одну позицию """ # Проверяем, что все резюме существуют existing_resumes = [] for resume_id in request.resume_ids: resume = await resume_repo.get_by_id(resume_id) if resume: existing_resumes.append(resume_id) if not existing_resumes: raise HTTPException(status_code=404, detail="No valid resumes found") # Запускаем задачу массового анализа task = analyze_multiple_candidates.delay(existing_resumes) return BulkAnalysisResponse( message="Bulk analysis started", resume_count=len(existing_resumes), task_id=task.id, ) @router.get("/ranking/{vacancy_id}") async def get_candidates_ranking( vacancy_id: int, resume_repo: ResumeRepository = Depends(ResumeRepository) ): """ Получить ранжированный список кандидатов для вакансии Сортирует кандидатов по результатам анализа интервью Показывает только тех, кто прошел интервью """ # Получаем все резюме для вакансии со статусом "interviewed" resumes = await resume_repo.get_by_vacancy_id(vacancy_id) interviewed_resumes = [r for r in resumes if r.status in ["interviewed"]] if not interviewed_resumes: return { "vacancy_id": vacancy_id, "candidates": [], "message": "No interviewed candidates found", } # Запускаем массовый анализ если еще не было resume_ids = [r.id for r in interviewed_resumes] task = analyze_multiple_candidates.delay(resume_ids) # В реальности здесь нужно дождаться выполнения или получить из кэша # Пока возвращаем информацию о запущенной задаче return { "vacancy_id": vacancy_id, "task_id": task.id, "message": f"Analysis started for {len(resume_ids)} candidates", "resume_ids": resume_ids, } @router.get("/report/{resume_id}") async def get_interview_report( resume_id: int, resume_repo: ResumeRepository = Depends(ResumeRepository) ): """ Получить готовый отчет анализа интервью Если отчет еще не готов - запускает анализ """ resume = await resume_repo.get_by_id(resume_id) if not resume: raise HTTPException(status_code=404, detail="Resume not found") # Проверяем, есть ли уже готовый отчет в notes if resume.notes and "ОЦЕНКА КАНДИДАТА" in resume.notes: return { "resume_id": resume_id, "candidate_name": resume.applicant_name, "status": "completed", "report_summary": resume.notes, "message": "Report available", } # Если отчета нет - запускаем анализ task = generate_interview_report.delay(resume_id) return { "resume_id": resume_id, "candidate_name": resume.applicant_name, "status": "in_progress", "task_id": task.id, "message": "Analysis started, check back later", } @router.get("/statistics/{vacancy_id}") async def get_analysis_statistics( vacancy_id: int, resume_repo: ResumeRepository = Depends(ResumeRepository) ): """ Получить статистику анализа кандидатов по вакансии """ resumes = await resume_repo.get_by_vacancy_id(vacancy_id) total_candidates = len(resumes) interviewed = len([r for r in resumes if r.status == "interviewed"]) with_reports = len( [r for r in resumes if r.notes and "ОЦЕНКА КАНДИДАТА" in r.notes] ) # Подсчитываем рекомендации из notes (упрощенно) recommendations = { "strongly_recommend": 0, "recommend": 0, "consider": 0, "reject": 0, } for resume in resumes: if resume.notes and "ОЦЕНКА КАНДИДАТА" in resume.notes: notes = resume.notes.lower() if "strongly_recommend" in notes: recommendations["strongly_recommend"] += 1 elif "recommend" in notes and "strongly_recommend" not in notes: recommendations["recommend"] += 1 elif "consider" in notes: recommendations["consider"] += 1 elif "reject" in notes: recommendations["reject"] += 1 return { "vacancy_id": vacancy_id, "statistics": { "total_candidates": total_candidates, "interviewed_candidates": interviewed, "analyzed_candidates": with_reports, "recommendations": recommendations, "analysis_completion": round((with_reports / max(interviewed, 1)) * 100, 1) if interviewed > 0 else 0, }, } @router.get("/pdf-report/{resume_id}") async def get_pdf_report( resume_id: int, session=Depends(get_session), resume_repo: ResumeRepository = Depends(ResumeRepository), ): """ Получить PDF отчет по интервью Если отчет готов - перенаправляет на S3 URL Если отчета нет - возвращает информацию о статусе """ from sqlmodel import select from app.models.interview import InterviewSession from app.models.interview_report import InterviewReport # Проверяем, существует ли резюме resume = await resume_repo.get_by_id(resume_id) if not resume: raise HTTPException(status_code=404, detail="Resume not found") # Ищем сессию интервью и отчет statement = ( select(InterviewReport, InterviewSession) .join( InterviewSession, InterviewReport.interview_session_id == InterviewSession.id, ) .where(InterviewSession.resume_id == resume_id) ) result = await session.exec(statement) report_session = result.first() if not report_session: # Если отчета нет - возможно, нужно запустить анализ raise HTTPException( status_code=404, detail="Interview report not found. Run analysis first using POST /analysis/interview-report/{resume_id}", ) report, interview_session = report_session if not report.pdf_report_url: # PDF еще не сгенерирован return { "status": "pdf_not_ready", "message": "PDF report is being generated or failed to generate", "report_id": report.id, "candidate_name": resume.applicant_name, } # Перенаправляем на S3 URL return RedirectResponse(url=report.pdf_report_url, status_code=302) @router.get("/report-data/{resume_id}") async def get_report_data( resume_id: int, session=Depends(get_session), resume_repo: ResumeRepository = Depends(ResumeRepository), ): """ Получить данные отчета в JSON формате (без PDF) """ from sqlmodel import select from app.models.interview import InterviewSession from app.models.interview_report import InterviewReport # Проверяем, существует ли резюме resume = await resume_repo.get_by_id(resume_id) if not resume: raise HTTPException(status_code=404, detail="Resume not found") # Ищем отчет statement = ( select(InterviewReport, InterviewSession) .join( InterviewSession, InterviewReport.interview_session_id == InterviewSession.id, ) .where(InterviewSession.resume_id == resume_id) ) result = await session.exec(statement) report_session = result.first() if not report_session: raise HTTPException(status_code=404, detail="Interview report not found") report, interview_session = report_session return { "report_id": report.id, "candidate_name": resume.applicant_name, "position": "Unknown Position", # Можно расширить через vacancy "interview_date": report.created_at.isoformat(), "overall_score": report.overall_score, "recommendation": report.recommendation.value, "scores": { "technical_skills": { "score": report.technical_skills_score, "justification": report.technical_skills_justification, "concerns": report.technical_skills_concerns, }, "experience_relevance": { "score": report.experience_relevance_score, "justification": report.experience_relevance_justification, "concerns": report.experience_relevance_concerns, }, "communication": { "score": report.communication_score, "justification": report.communication_justification, "concerns": report.communication_concerns, }, "problem_solving": { "score": report.problem_solving_score, "justification": report.problem_solving_justification, "concerns": report.problem_solving_concerns, }, "cultural_fit": { "score": report.cultural_fit_score, "justification": report.cultural_fit_justification, "concerns": report.cultural_fit_concerns, }, }, "strengths": report.strengths, "weaknesses": report.weaknesses, "red_flags": report.red_flags, "next_steps": report.next_steps, "metrics": { "interview_duration_minutes": report.interview_duration_minutes, "dialogue_messages_count": report.dialogue_messages_count, "questions_quality_score": report.questions_quality_score, }, "pdf_available": bool(report.pdf_report_url), "pdf_url": report.pdf_report_url, "analysis_metadata": { "method": report.analysis_method, "model_used": report.llm_model_used, "analysis_duration": report.analysis_duration_seconds, }, }