ai-hackaton-backend/app/services/admin_service.py

319 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from typing import Annotated
from fastapi import Depends
from app.repositories.interview_repository import InterviewRepository
from app.repositories.resume_repository import ResumeRepository
from app.services.interview_finalization_service import InterviewFinalizationService
from app.services.interview_service import InterviewRoomService
class AdminService:
def __init__(
self,
interview_repo: Annotated[InterviewRepository, Depends(InterviewRepository)],
resume_repo: Annotated[ResumeRepository, Depends(ResumeRepository)],
interview_service: Annotated[
InterviewRoomService, Depends(InterviewRoomService)
],
finalization_service: Annotated[
InterviewFinalizationService, Depends(InterviewFinalizationService)
],
):
self.interview_repo = interview_repo
self.resume_repo = resume_repo
self.interview_service = interview_service
self.finalization_service = finalization_service
async def get_active_interview_processes(self):
"""Получить список активных AI процессов"""
active_sessions = await self.interview_service.get_active_agent_processes()
import psutil
processes_info = []
for session in active_sessions:
process_info = {
"session_id": session.id,
"resume_id": session.resume_id,
"room_name": session.room_name,
"pid": session.ai_agent_pid,
"status": session.ai_agent_status,
"started_at": session.started_at.isoformat()
if session.started_at
else None,
"is_running": False,
"memory_mb": 0,
"cpu_percent": 0,
}
if session.ai_agent_pid:
try:
process = psutil.Process(session.ai_agent_pid)
if process.is_running():
process_info["is_running"] = True
process_info["memory_mb"] = round(
process.memory_info().rss / 1024 / 1024, 1
)
process_info["cpu_percent"] = round(
process.cpu_percent(interval=0.1), 1
)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
processes_info.append(process_info)
return {
"active_processes": len([p for p in processes_info if p["is_running"]]),
"total_sessions": len(processes_info),
"processes": processes_info,
}
async def stop_interview_process(self, session_id: int):
"""Остановить AI процесс интервью"""
success = await self.interview_service.stop_agent_process(session_id)
return {
"success": success,
"message": f"Process for session {session_id} {'stopped' if success else 'failed to stop'}",
}
async def cleanup_dead_processes(self):
"""Очистить информацию о мертвых процессах"""
cleaned_count = await self.finalization_service.cleanup_dead_processes()
return {
"cleaned_processes": cleaned_count,
"message": f"Cleaned up {cleaned_count} dead processes",
}
async def get_analytics_dashboard(self) -> dict:
"""Основная аналитическая панель"""
all_resumes = await self.resume_repo.get_all()
status_stats = {}
for resume in all_resumes:
status = (
resume.status.value
if hasattr(resume.status, "value")
else str(resume.status)
)
status_stats[status] = status_stats.get(status, 0) + 1
analyzed_count = 0
recommendation_stats = {
"strongly_recommend": 0,
"recommend": 0,
"consider": 0,
"reject": 0,
}
for resume in all_resumes:
if resume.notes and "ОЦЕНКА КАНДИДАТА" in resume.notes:
analyzed_count += 1
notes = resume.notes.lower()
if "strongly_recommend" in notes:
recommendation_stats["strongly_recommend"] += 1
elif "recommend" in notes and "strongly_recommend" not in notes:
recommendation_stats["recommend"] += 1
elif "consider" in notes:
recommendation_stats["consider"] += 1
elif "reject" in notes:
recommendation_stats["reject"] += 1
recent_resumes = sorted(all_resumes, key=lambda x: x.updated_at, reverse=True)[
:10
]
recent_activity = []
for resume in recent_resumes:
activity_item = {
"resume_id": resume.id,
"candidate_name": resume.applicant_name,
"status": resume.status.value
if hasattr(resume.status, "value")
else str(resume.status),
"updated_at": resume.updated_at.isoformat()
if resume.updated_at
else None,
"has_analysis": resume.notes and "ОЦЕНКА КАНДИДАТА" in resume.notes,
}
recent_activity.append(activity_item)
return {
"summary": {
"total_candidates": len(all_resumes),
"interviewed_candidates": status_stats.get("interviewed", 0),
"analyzed_candidates": analyzed_count,
"analysis_completion_rate": round(
(analyzed_count / max(len(all_resumes), 1)) * 100, 1
),
},
"status_distribution": status_stats,
"recommendation_distribution": recommendation_stats,
"recent_activity": recent_activity,
}
async def get_vacancy_analytics(self, vacancy_id: int) -> dict:
"""Аналитика кандидатов по конкретной вакансии"""
vacancy_resumes = await self.resume_repo.get_by_vacancy_id(vacancy_id)
if not vacancy_resumes:
return {
"vacancy_id": vacancy_id,
"message": "No candidates found for this vacancy",
"candidates": [],
}
candidates_info = []
for resume in vacancy_resumes:
overall_score = None
recommendation = None
if resume.notes and "ОЦЕНКА КАНДИДАТА" in resume.notes:
notes = resume.notes
if "Общий балл:" in notes:
try:
score_line = [
line for line in notes.split("\n") if "Общий балл:" in line
][0]
overall_score = int(
score_line.split("Общий балл:")[1].split("/")[0].strip()
)
except:
pass
if "Рекомендация:" in notes:
try:
rec_line = [
line
for line in notes.split("\n")
if "Рекомендация:" in line
][0]
recommendation = rec_line.split("Рекомендация:")[1].strip()
except:
pass
candidate_info = {
"resume_id": resume.id,
"candidate_name": resume.applicant_name,
"email": resume.applicant_email,
"status": resume.status.value
if hasattr(resume.status, "value")
else str(resume.status),
"created_at": resume.created_at.isoformat()
if resume.created_at
else None,
"updated_at": resume.updated_at.isoformat()
if resume.updated_at
else None,
"has_analysis": resume.notes and "ОЦЕНКА КАНДИДАТА" in resume.notes,
"overall_score": overall_score,
"recommendation": recommendation,
"has_parsed_data": bool(resume.parsed_data),
"has_interview_plan": bool(resume.interview_plan),
}
candidates_info.append(candidate_info)
candidates_info.sort(
key=lambda x: (x["overall_score"] or 0, x["updated_at"] or ""), reverse=True
)
return {
"vacancy_id": vacancy_id,
"total_candidates": len(candidates_info),
"candidates": candidates_info,
}
async def generate_reports_for_vacancy(self, vacancy_id: int) -> dict:
"""Запустить генерацию отчетов для всех кандидатов вакансии"""
from celery_worker.interview_analysis_task import analyze_multiple_candidates
vacancy_resumes = await self.resume_repo.get_by_vacancy_id(vacancy_id)
interviewed_resumes = [
r for r in vacancy_resumes if r.status in ["interviewed"]
]
if not interviewed_resumes:
return {
"error": "No interviewed candidates found for this vacancy",
"vacancy_id": vacancy_id,
}
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,
}
async def get_system_stats(self) -> dict:
"""Общая статистика системы"""
import psutil
try:
cpu_percent = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
disk = psutil.disk_usage("/")
python_processes = []
for proc in psutil.process_iter(
["pid", "name", "memory_info", "cpu_percent", "cmdline"]
):
try:
if proc.info["name"] and "python" in proc.info["name"].lower():
cmdline = (
" ".join(proc.info["cmdline"])
if proc.info["cmdline"]
else ""
)
if "ai_interviewer_agent" in cmdline:
python_processes.append(
{
"pid": proc.info["pid"],
"memory_mb": round(
proc.info["memory_info"].rss / 1024 / 1024, 1
),
"cpu_percent": proc.info["cpu_percent"] or 0,
"cmdline": cmdline,
}
)
except (
psutil.NoSuchProcess,
psutil.AccessDenied,
psutil.ZombieProcess,
):
pass
return {
"system": {
"cpu_percent": cpu_percent,
"memory_percent": memory.percent,
"memory_available_gb": round(
memory.available / 1024 / 1024 / 1024, 1
),
"disk_percent": disk.percent,
"disk_free_gb": round(disk.free / 1024 / 1024 / 1024, 1),
},
"ai_agents": {
"count": len(python_processes),
"total_memory_mb": sum(p["memory_mb"] for p in python_processes),
"processes": python_processes,
},
}
except Exception as e:
return {"error": f"Error getting system stats: {str(e)}"}