ai-hackaton-backend/celery_worker/tasks.py
2025-09-03 14:36:27 +05:00

412 lines
18 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.

import os
import json
from typing import Dict, Any
from celery import current_task
from datetime import datetime
from celery_worker.celery_app import celery_app
from celery_worker.database import get_sync_session, SyncResumeRepository
from rag.llm.model import ResumeParser
from rag.registry import registry
def generate_interview_plan(resume_id: int, combined_data: Dict[str, Any]) -> Dict[str, Any]:
"""Генерирует план интервью на основе резюме и вакансии"""
try:
# Получаем данные о вакансии из БД
with get_sync_session() as session:
repo = SyncResumeRepository(session)
resume_record = repo.get_by_id(resume_id)
if not resume_record:
return None
# Здесь нужно получить данные вакансии
# Пока используем заглушку, потом добавим связь с vacancy
vacancy_data = {
"title": "Python Developer",
"requirements": "Python, FastAPI, PostgreSQL, Docker",
"company_name": "Tech Company",
"experience_level": "Middle"
}
# Генерируем план через LLM
chat_model = registry.get_chat_model()
plan_prompt = f"""
Создай детальный план интервью для кандидата на основе его резюме и требований вакансии.
РЕЗЮМЕ КАНДИДАТА:
- Имя: {combined_data.get('name', 'Не указано')}
- Навыки: {', '.join(combined_data.get('skills', []))}
- Опыт: {combined_data.get('total_years', 0)} лет
- Образование: {combined_data.get('education', 'Не указано')}
ВАКАНСИЯ:
- Позиция: {vacancy_data['title']}
- Требования: {vacancy_data['requirements']}
- Компания: {vacancy_data['company_name']}
- Уровень: {vacancy_data['experience_level']}
Создай план интервью в формате JSON:
{{
"interview_structure": {{
"duration_minutes": 45,
"greeting": "Краткое приветствие и знакомство (3 мин)",
"sections": [
{{
"name": "Знакомство с кандидатом",
"duration_minutes": 5,
"questions": ["Расскажи о себе", "Что привлекло в этой позиции?"]
}},
{{
"name": "Технические навыки",
"duration_minutes": 20,
"questions": ["Опыт с Python", "Работа с базами данных"]
}},
{{
"name": "Опыт и проекты",
"duration_minutes": 15,
"questions": ["Расскажи о сложном проекте", "Как решаешь проблемы?"]
}},
{{
"name": "Вопросы кандидата",
"duration_minutes": 2,
"questions": ["Есть ли вопросы ко мне?"]
}}
]
}},
"focus_areas": ["technical_skills", "problem_solving", "cultural_fit"],
"key_evaluation_points": [
"Глубина знаний Python",
"Опыт командной работы",
"Мотивация к изучению нового"
],
"red_flags_to_check": [],
"personalization_notes": "Кандидат имеет хороший технический опыт"
}}
"""
from langchain.schema import HumanMessage, SystemMessage
messages = [
SystemMessage(content="Ты HR эксперт по планированию интервью. Создавай структурированные планы."),
HumanMessage(content=plan_prompt)
]
response = chat_model.get_llm().invoke(messages)
response_text = response.content.strip()
# Парсим JSON ответ
if response_text.startswith('{') and response_text.endswith('}'):
return json.loads(response_text)
else:
# Ищем JSON в тексте
start = response_text.find('{')
end = response_text.rfind('}') + 1
if start != -1 and end > start:
return json.loads(response_text[start:end])
return None
except Exception as e:
print(f"Ошибка генерации плана интервью: {str(e)}")
return None
@celery_app.task(bind=True)
def parse_resume_task(self, resume_id: str, file_path: str):
"""
Асинхронная задача парсинга резюме
Args:
resume_id: ID резюме
file_path: Путь к PDF файлу резюме
"""
try:
# Шаг 0: Обновляем статус в БД - начали парсинг
with get_sync_session() as session:
repo = SyncResumeRepository(session)
repo.update_status(int(resume_id), 'parsing')
# Обновляем статус задачи
self.update_state(
state='PENDING',
meta={'status': 'Начинаем парсинг резюме...', 'progress': 10}
)
# Инициализируем модели из registry
try:
chat_model = registry.get_chat_model()
embeddings_model = registry.get_embeddings_model()
vector_store = registry.get_vector_store()
except Exception as e:
# Обновляем статус в БД - ошибка инициализации
with get_sync_session() as session:
repo = SyncResumeRepository(session)
repo.update_status(int(resume_id), 'failed', error_message=f"Ошибка инициализации моделей: {str(e)}")
raise Exception(f"Ошибка инициализации моделей: {str(e)}")
# Шаг 1: Парсинг резюме
self.update_state(
state='PROGRESS',
meta={'status': 'Извлекаем текст из PDF...', 'progress': 20}
)
parser = ResumeParser(chat_model)
if not os.path.exists(file_path):
# Обновляем статус в БД - файл не найден
with get_sync_session() as session:
repo = SyncResumeRepository(session)
repo.update_status(int(resume_id), 'failed', error_message=f"Файл не найден: {file_path}")
raise Exception(f"Файл не найден: {file_path}")
parsed_resume = parser.parse_resume_from_file(file_path)
# Получаем оригинальные данные из формы
with get_sync_session() as session:
repo = SyncResumeRepository(session)
resume_record = repo.get_by_id(int(resume_id))
if not resume_record:
raise Exception(f"Резюме с ID {resume_id} не найдено в базе данных")
# Извлекаем нужные данные пока сессия активна
applicant_name = resume_record.applicant_name
applicant_email = resume_record.applicant_email
applicant_phone = resume_record.applicant_phone
# Создаем комбинированные данные: навыки и опыт из парсинга, контакты из формы
combined_data = parsed_resume.copy()
combined_data['name'] = applicant_name
combined_data['email'] = applicant_email
combined_data['phone'] = applicant_phone or parsed_resume.get('phone', '')
# Шаг 2: Векторизация и сохранение в Milvus
self.update_state(
state='PENDING',
meta={'status': 'Сохраняем в векторную базу...', 'progress': 60}
)
vector_store.add_candidate_profile(str(resume_id), combined_data)
# Шаг 3: Обновляем статус в PostgreSQL - успешно обработано
self.update_state(
state='PENDING',
meta={'status': 'Обновляем статус в базе данных...', 'progress': 85}
)
# Шаг 4: Генерируем план интервью
self.update_state(
state='PENDING',
meta={'status': 'Генерируем план интервью...', 'progress': 90}
)
interview_plan = generate_interview_plan(int(resume_id), combined_data)
with get_sync_session() as session:
repo = SyncResumeRepository(session)
repo.update_status(int(resume_id), 'parsed', parsed_data=combined_data)
# Сохраняем план интервью
if interview_plan:
repo.update_interview_plan(int(resume_id), interview_plan)
# Завершено успешно
self.update_state(
state='SUCCESS',
meta={
'status': 'Резюме успешно обработано и план интервью готов',
'progress': 100,
'result': combined_data
}
)
return {
'resume_id': resume_id,
'status': 'completed',
'parsed_data': combined_data
}
except Exception as e:
# В случае ошибки
self.update_state(
state='FAILURE',
meta={
'status': f'Ошибка при обработке резюме: {str(e)}',
'progress': 0,
'error': str(e)
}
)
# Обновляем статус в БД как failed
try:
with get_sync_session() as session:
repo = SyncResumeRepository(session)
repo.update_status(int(resume_id), 'failed', error_message=str(e))
except Exception as db_error:
print(f"Ошибка при обновлении статуса в БД: {str(db_error)}")
raise
# Функция больше не нужна - используем SyncResumeRepository напрямую
@celery_app.task(bind=True)
def generate_interview_questions_task(self, resume_id: str, job_description: str):
"""
Генерация персонализированных вопросов для интервью на основе резюме и описания вакансии
Args:
resume_id: ID резюме
job_description: Описание вакансии
"""
try:
self.update_state(
state='PENDING',
meta={'status': 'Начинаем генерацию вопросов...', 'progress': 10}
)
# Инициализируем модели
try:
chat_model = registry.get_chat_model()
vector_store = registry.get_vector_store()
except Exception as e:
raise Exception(f"Ошибка инициализации моделей: {str(e)}")
# Шаг 1: Получить parsed резюме из базы данных
self.update_state(
state='PENDING',
meta={'status': 'Получаем данные резюме...', 'progress': 20}
)
with get_sync_session() as session:
repo = SyncResumeRepository(session)
resume = repo.get_by_id(int(resume_id))
if not resume:
raise Exception(f"Резюме с ID {resume_id} не найдено")
if not resume.parsed_data:
raise Exception(f"Резюме {resume_id} еще не обработано")
# Шаг 2: Получить похожие кандидатов из Milvus для анализа
self.update_state(
state='PENDING',
meta={'status': 'Анализируем профиль кандидата...', 'progress': 40}
)
candidate_skills = " ".join(resume.parsed_data.get('skills', []))
similar_candidates = vector_store.search_similar_candidates(candidate_skills, k=3)
# Шаг 3: Сгенерировать персонализированные вопросы через LLM
self.update_state(
state='PENDING',
meta={'status': 'Генерируем вопросы для интервью...', 'progress': 70}
)
questions_prompt = f"""
Сгенерируй 10 персонализированных вопросов для интервью кандидата на основе его резюме и описания вакансии.
РЕЗЮМЕ КАНДИДАТА:
Имя: {resume.parsed_data.get('name', 'Не указано')}
Навыки: {', '.join(resume.parsed_data.get('skills', []))}
Опыт работы: {resume.parsed_data.get('total_years', 0)} лет
Образование: {resume.parsed_data.get('education', 'Не указано')}
ОПИСАНИЕ ВАКАНСИИ:
{job_description}
ИНСТРУКЦИИ:
1. Задавай вопросы, которые помогут оценить технические навыки кандидата
2. Включи вопросы о конкретном опыте работы из резюме
3. Добавь вопросы на соответствие требованиям вакансии
4. Включи 2-3 поведенческих вопроса
5. Верни ответ в JSON формате
Формат ответа:
{{
"questions": [
{{
"id": 1,
"category": "technical|experience|behavioral|vacancy_specific",
"question": "Текст вопроса",
"reasoning": "Почему этот вопрос важен для данного кандидата"
}}
]
}}
"""
from langchain.schema import HumanMessage, SystemMessage
messages = [
SystemMessage(content="Ты эксперт по проведению технических интервью. Генерируй качественные, персонализированные вопросы."),
HumanMessage(content=questions_prompt)
]
response = chat_model.get_llm().invoke(messages)
# Парсим ответ
import json
response_text = response.content.strip()
# Извлекаем JSON из ответа
if response_text.startswith('{') and response_text.endswith('}'):
questions_data = json.loads(response_text)
else:
# Ищем JSON внутри текста
start = response_text.find('{')
end = response_text.rfind('}') + 1
if start != -1 and end > start:
json_str = response_text[start:end]
questions_data = json.loads(json_str)
else:
raise ValueError("JSON не найден в ответе LLM")
# Шаг 4: Сохранить вопросы в notes резюме (пока так, потом можно создать отдельную таблицу)
self.update_state(
state='PENDING',
meta={'status': 'Сохраняем вопросы...', 'progress': 90}
)
with get_sync_session() as session:
repo = SyncResumeRepository(session)
resume = repo.get_by_id(int(resume_id))
if resume:
# Сохраняем вопросы в notes (временно)
existing_notes = resume.notes or ""
interview_questions = json.dumps(questions_data, ensure_ascii=False, indent=2)
resume.notes = f"{existing_notes}\n\nINTERVIEW QUESTIONS:\n{interview_questions}"
from datetime import datetime
resume.updated_at = datetime.utcnow()
session.add(resume)
# Завершено успешно
self.update_state(
state='SUCCESS',
meta={
'status': 'Вопросы для интервью успешно сгенерированы',
'progress': 100,
'result': questions_data
}
)
return {
'resume_id': resume_id,
'status': 'questions_generated',
'questions': questions_data['questions']
}
except Exception as e:
# В случае ошибки
self.update_state(
state='FAILURE',
meta={
'status': f'Ошибка при генерации вопросов: {str(e)}',
'progress': 0,
'error': str(e)
}
)
raise Exception(f"Ошибка при генерации вопросов: {str(e)}")