412 lines
18 KiB
Python
412 lines
18 KiB
Python
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)}") |