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)}")