upd promts; fix reports

This commit is contained in:
Даниил Ивлев 2025-09-08 14:07:09 +05:00
parent 8d449af338
commit 9128bb8881
5 changed files with 141 additions and 98 deletions

View File

@ -65,9 +65,8 @@ class InterviewAgent:
self.interview_finalized = False # Флаг завершения интервью self.interview_finalized = False # Флаг завершения интервью
# Трекинг времени интервью # Трекинг времени интервью
import time self.interview_start_time = None # Устанавливается при фактическом старте
self.interview_end_time = None # Устанавливается при завершении
self.interview_start_time = time.time()
self.duration_minutes = interview_plan.get("interview_structure", {}).get( self.duration_minutes = interview_plan.get("interview_structure", {}).get(
"duration_minutes", 10 "duration_minutes", 10
) )
@ -77,10 +76,6 @@ class InterviewAgent:
) )
self.total_sections = len(self.sections) self.total_sections = len(self.sections)
logger.info(
f"[TIME] Interview started at {time.strftime('%H:%M:%S')}, duration: {self.duration_minutes} min"
)
def get_current_section(self) -> dict: def get_current_section(self) -> dict:
"""Получить текущую секцию интервью""" """Получить текущую секцию интервью"""
if self.current_section < len(self.sections): if self.current_section < len(self.sections):
@ -126,11 +121,10 @@ class InterviewAgent:
key_evaluation_points = self.interview_plan.get("key_evaluation_points", []) key_evaluation_points = self.interview_plan.get("key_evaluation_points", [])
# Вычисляем текущее время интервью # Вычисляем текущее время интервью
import time time_info = self.get_time_info()
elapsed_minutes = time_info["elapsed_minutes"]
elapsed_minutes = (time.time() - self.interview_start_time) / 60 remaining_minutes = time_info["remaining_minutes"]
remaining_minutes = max(0, self.duration_minutes - elapsed_minutes) time_percentage = time_info["time_percentage"]
time_percentage = min(100, (elapsed_minutes / self.duration_minutes) * 100)
# Формируем план интервью для агента # Формируем план интервью для агента
sections_info = "\n".join( sections_info = "\n".join(
@ -193,7 +187,7 @@ class InterviewAgent:
- Контактное лицо: {self.vacancy_data.get('contacts_name') or 'Не указано'}""" - Контактное лицо: {self.vacancy_data.get('contacts_name') or 'Не указано'}"""
return f""" return f"""
Ты опытный HR-интервьюер, который проводит адаптивное голосовое собеседование. Представься контактным именем из вакансии (если оно есть) Ты опытный HR-интервьюер Стефани, который проводит адаптивное голосовое собеседование. Представься контактным именем из вакансии (если оно есть)
ИНФОРМАЦИЯ О ВАКАНСИИ: ИНФОРМАЦИЯ О ВАКАНСИИ:
@ -233,6 +227,10 @@ class InterviewAgent:
Проблемные / кейсы (20%) проверить мышление и подход к решению. Проблемные / кейсы (20%) проверить мышление и подход к решению.
Пример: "У нас есть система, которая падает раз в неделю. Как бы ты подошёл к диагностике проблемы?" Пример: "У нас есть система, которая падает раз в неделю. Как бы ты подошёл к диагностике проблемы?"
Задавай вопросы кратко и понятно. Не вываливай кучу информации на человека.
Не перечисляй человеку все пункты и вопросы из секции. Предлагай один общий вопрос или задавай уточняющие по по очереди.
Ты должна спрашивать вопросы максимум в 3 предложения
ВРЕМЯ ИНТЕРВЬЮ: ВРЕМЯ ИНТЕРВЬЮ:
- Запланированная длительность: {self.duration_minutes} минут - Запланированная длительность: {self.duration_minutes} минут
- Прошло времени: {elapsed_minutes:.1f} минут ({time_percentage:.0f}%) - Прошло времени: {elapsed_minutes:.1f} минут ({time_percentage:.0f}%)
@ -286,7 +284,15 @@ class InterviewAgent:
"""Получает информацию о времени интервью""" """Получает информацию о времени интервью"""
import time import time
elapsed_minutes = (time.time() - self.interview_start_time) / 60 if self.interview_start_time is None:
# Интервью еще не началось
elapsed_minutes = 0.0
remaining_minutes = float(self.duration_minutes)
time_percentage = 0.0
else:
# Интервью идет
current_time = self.interview_end_time or time.time()
elapsed_minutes = (current_time - self.interview_start_time) / 60
remaining_minutes = max(0.0, self.duration_minutes - elapsed_minutes) remaining_minutes = max(0.0, self.duration_minutes - elapsed_minutes)
time_percentage = min(100.0, (elapsed_minutes / self.duration_minutes) * 100) time_percentage = min(100.0, (elapsed_minutes / self.duration_minutes) * 100)
@ -421,8 +427,9 @@ async def entrypoint(ctx: JobContext):
# TTS # TTS
tts = ( tts = (
openai.TTS( openai.TTS(
model="gpt-4o-mini-tts", model="tts-1-hd",
api_key=settings.openai_api_key, api_key=settings.openai_api_key,
voice='coral'
) )
if settings.openai_api_key if settings.openai_api_key
else silero.TTS(language="ru", model="v4_ru") else silero.TTS(language="ru", model="v4_ru")
@ -474,6 +481,18 @@ async def entrypoint(ctx: JobContext):
interviewer_instance.interview_finalized = True interviewer_instance.interview_finalized = True
# Устанавливаем время завершения интервью
import time
interviewer_instance.interview_end_time = time.time()
if interviewer_instance.interview_start_time:
total_minutes = (interviewer_instance.interview_end_time - interviewer_instance.interview_start_time) / 60
logger.info(
f"[TIME] Interview ended at {time.strftime('%H:%M:%S')}, total duration: {total_minutes:.1f} min"
)
else:
logger.info(f"[TIME] Interview ended at {time.strftime('%H:%M:%S')} (no start time recorded)")
try: try:
logger.info( logger.info(
f"[FINALIZE] Starting interview finalization for room: {room_name}" f"[FINALIZE] Starting interview finalization for room: {room_name}"
@ -543,11 +562,13 @@ async def entrypoint(ctx: JobContext):
# --- Мониторинг команд завершения --- # --- Мониторинг команд завершения ---
async def monitor_end_commands(): async def monitor_end_commands():
"""Мониторит команды завершения сессии""" """Мониторит команды завершения сессии и лимит времени"""
command_file = "agent_commands.json" command_file = "agent_commands.json"
TIME_LIMIT_MINUTES = 60 # Жесткий лимит времени интервью
while not interviewer.interview_finalized: while not interviewer.interview_finalized:
try: try:
# Проверяем команды завершения
if os.path.exists(command_file): if os.path.exists(command_file):
with open(command_file, encoding="utf-8") as f: with open(command_file, encoding="utf-8") as f:
command = json.load(f) command = json.load(f)
@ -566,71 +587,50 @@ async def entrypoint(ctx: JobContext):
) )
break break
await asyncio.sleep(1) # Проверяем каждые 1 секунды # Проверяем превышение лимита времени
if interviewer.interview_start_time is not None:
time_info = interviewer.get_time_info()
if time_info["elapsed_minutes"] >= TIME_LIMIT_MINUTES:
logger.warning(
f"[TIME_LIMIT] Interview exceeded {TIME_LIMIT_MINUTES} minutes "
f"({time_info['elapsed_minutes']:.1f} min), forcing completion"
)
if not interviewer.interview_finalized:
await complete_interview_sequence(
ctx.room.name, interviewer
)
break
await asyncio.sleep(2) # Проверяем каждые 5 секунд
except Exception as e: except Exception as e:
logger.error(f"[COMMAND] Error monitoring commands: {str(e)}") logger.error(f"[COMMAND] Error monitoring commands: {str(e)}")
await asyncio.sleep(5) await asyncio.sleep(2)
# Запускаем мониторинг команд в фоне # Запускаем мониторинг команд в фоне
asyncio.create_task(monitor_end_commands()) asyncio.create_task(monitor_end_commands())
# --- Обработчик состояния пользователя (замена мониторинга тишины) ---
disconnect_timer: asyncio.Task | None = None
@session.on("user_state_changed") @session.on("user_state_changed")
def on_user_state_changed(event): def on_user_state_changed(event):
"""Обработчик изменения состояния пользователя (активен/неактивен)""" """Обработчик изменения состояния пользователя (активен/неактивен)"""
async def on_change(): async def on_change():
nonlocal disconnect_timer
logger.info(f"[USER_STATE] User state changed to: {event.new_state}") logger.info(f"[USER_STATE] User state changed to: {event.new_state}")
# === Пользователь начал говорить ===
if event.new_state == "speaking":
# Если есть таймер на 30 секунд — отменяем его
if disconnect_timer is not None:
logger.info("[USER_STATE] Cancelling disconnect timer due to speaking")
disconnect_timer.cancel()
disconnect_timer = None
# === Пользователь молчит более 10 секунд (state == away) === # === Пользователь молчит более 10 секунд (state == away) ===
elif event.new_state == "away" and interviewer.intro_done: if event.new_state == "away" and interviewer.intro_done:
logger.info("[USER_STATE] User away detected, sending check-in message...") logger.info("[USER_STATE] User away detected, sending check-in message...")
# 1) Первое сообщение — проверка связи # сообщение — проверка связи
handle = await session.generate_reply( await session.generate_reply(
instructions=( instructions=(
"Клиент молчит уже больше 10 секунд. " "Клиент молчит уже больше 10 секунд. "
"Проверь связь фразой вроде 'Приём! Ты меня слышишь?' " "Проверь связь фразой вроде 'Приём! Ты меня слышишь?' "
"или 'Связь не пропала?'" "или 'Связь не пропала?'"
) )
) )
await handle # ждем завершения первой реплики
# 2) Таймер на 30 секунд
async def disconnect_timeout():
try:
await asyncio.sleep(30)
logger.info("[DISCONNECT_TIMER] 30 seconds passed, sending disconnect message")
# Второе сообщение — считаем, что клиент отключился
await session.generate_reply(
instructions="Похоже клиент отключился"
)
logger.info("[DISCONNECT_TIMER] Disconnect message sent successfully")
except asyncio.CancelledError:
logger.info("[DISCONNECT_TIMER] Timer cancelled before completion")
except Exception as e:
logger.error(f"[DISCONNECT_TIMER] Error in disconnect timeout: {e}")
# 3) Если уже есть активный таймер — отменяем его перед запуском нового
if disconnect_timer is not None:
disconnect_timer.cancel()
disconnect_timer = asyncio.create_task(disconnect_timeout())
asyncio.create_task(on_change()) asyncio.create_task(on_change())
@ -705,6 +705,12 @@ async def entrypoint(ctx: JobContext):
# Обновляем прогресс интервью # Обновляем прогресс интервью
if not interviewer.intro_done: if not interviewer.intro_done:
interviewer.intro_done = True interviewer.intro_done = True
# Устанавливаем время начала интервью при первом сообщении
import time
interviewer.interview_start_time = time.time()
logger.info(
f"[TIME] Interview started at {time.strftime('%H:%M:%S')}, duration: {interviewer.duration_minutes} min"
)
# Обновляем счетчик сообщений и треким время # Обновляем счетчик сообщений и треким время
interviewer.questions_asked_total += 1 interviewer.questions_asked_total += 1

View File

@ -233,7 +233,7 @@ class InterviewRoomService:
# Если плана нет, создаем базовый план на основе имеющихся данных # Если плана нет, создаем базовый план на основе имеющихся данных
fallback_plan = { fallback_plan = {
"interview_structure": { "interview_structure": {
"duration_minutes": 30, "duration_minutes": 45,
"greeting": f"Привет, {resume.applicant_name}! Готов к интервью?", "greeting": f"Привет, {resume.applicant_name}! Готов к интервью?",
"sections": [ "sections": [
{ {

View File

@ -28,6 +28,21 @@ class PDFReportService:
self.styles = getSampleStyleSheet() self.styles = getSampleStyleSheet()
self._setup_custom_styles() self._setup_custom_styles()
def _format_list_field(self, field_value) -> str:
"""Форматирует поле со списком для отображения в PDF"""
if not field_value:
return ""
if isinstance(field_value, list):
# Если это список, объединяем элементы
return "\n".join([""] + field_value)
elif isinstance(field_value, str):
# Если это строка, возвращаем как есть
return field_value
else:
# Для других типов конвертируем в строку
return str(field_value)
def _register_fonts(self): def _register_fonts(self):
"""Регистрация шрифтов для поддержки кириллицы""" """Регистрация шрифтов для поддержки кириллицы"""
try: try:
@ -212,31 +227,31 @@ class PDFReportService:
Paragraph("Технические навыки", table_text_style), Paragraph("Технические навыки", table_text_style),
Paragraph(f"{report.technical_skills_score}/100", table_text_style), Paragraph(f"{report.technical_skills_score}/100", table_text_style),
Paragraph(report.technical_skills_justification or "", table_text_style), Paragraph(report.technical_skills_justification or "", table_text_style),
Paragraph(report.technical_skills_concerns or "", table_text_style), Paragraph(self._format_list_field(report.technical_skills_concerns), table_text_style),
], ],
[ [
Paragraph("Релевантность опыта", table_text_style), Paragraph("Релевантность опыта", table_text_style),
Paragraph(f"{report.experience_relevance_score}/100", table_text_style), Paragraph(f"{report.experience_relevance_score}/100", table_text_style),
Paragraph(report.experience_relevance_justification or "", table_text_style), Paragraph(report.experience_relevance_justification or "", table_text_style),
Paragraph(report.experience_relevance_concerns or "", table_text_style), Paragraph(self._format_list_field(report.experience_relevance_concerns), table_text_style),
], ],
[ [
Paragraph("Коммуникация", table_text_style), Paragraph("Коммуникация", table_text_style),
Paragraph(f"{report.communication_score}/100", table_text_style), Paragraph(f"{report.communication_score}/100", table_text_style),
Paragraph(report.communication_justification or "", table_text_style), Paragraph(report.communication_justification or "", table_text_style),
Paragraph(report.communication_concerns or "", table_text_style), Paragraph(self._format_list_field(report.communication_concerns), table_text_style),
], ],
[ [
Paragraph("Решение задач", table_text_style), Paragraph("Решение задач", table_text_style),
Paragraph(f"{report.problem_solving_score}/100", table_text_style), Paragraph(f"{report.problem_solving_score}/100", table_text_style),
Paragraph(report.problem_solving_justification or "", table_text_style), Paragraph(report.problem_solving_justification or "", table_text_style),
Paragraph(report.problem_solving_concerns or "", table_text_style), Paragraph(self._format_list_field(report.problem_solving_concerns), table_text_style),
], ],
[ [
Paragraph("Культурное соответствие", table_text_style), Paragraph("Культурное соответствие", table_text_style),
Paragraph(f"{report.cultural_fit_score}/100", table_text_style), Paragraph(f"{report.cultural_fit_score}/100", table_text_style),
Paragraph(report.cultural_fit_justification or "", table_text_style), Paragraph(report.cultural_fit_justification or "", table_text_style),
Paragraph(report.cultural_fit_concerns or "", table_text_style), Paragraph(self._format_list_field(report.cultural_fit_concerns), table_text_style),
], ],
] ]
@ -343,15 +358,15 @@ class PDFReportService:
) )
for weakness in report.weaknesses: for weakness in report.weaknesses:
story.append(Paragraph(f"{weakness}", self.styles["CustomBodyText"])) story.append(Paragraph(f"{weakness}", self.styles["CustomBodyText"]))
story.append(Spacer(1, 10)) story.append(Spacer(1, 15))
# Красные флаги # Красные флаги
if report.red_flags: if report.red_flags:
story.append(Paragraph("Важные риски:", self.styles["SubHeader"])) story.append(Paragraph("Красные флаги:", self.styles["SectionHeader"]))
for red_flag in report.red_flags: for red_flag in report.red_flags:
story.append( story.append(
Paragraph( Paragraph(
f"{red_flag}", f"{red_flag}",
ParagraphStyle( ParagraphStyle(
name="RedFlag", name="RedFlag",
parent=self.styles["CustomBodyText"], parent=self.styles["CustomBodyText"],

View File

@ -385,19 +385,21 @@ def _call_openai_for_evaluation(context: str) -> dict | None:
И топ 3 сильные/слабые стороны. И топ 3 сильные/слабые стороны.
И red_flags (если есть): расхождение в стаже и опыте резюме и собеседования, шаблонные ответы, уклонение от вопросов
ОТВЕТЬ СТРОГО В JSON ФОРМАТЕ с обязательными полями: ОТВЕТЬ СТРОГО В JSON ФОРМАТЕ с обязательными полями:
- scores: объект с 5 критериями, каждый содержит score, justification, concerns - scores: объект с 5 критериями, каждый содержит score, justification, concerns
- overall_score: число от 0 до 100 (среднее арифметическое всех scores) - overall_score: число от 0 до 100 (среднее арифметическое всех scores)
- recommendation: одно из 4 значений выше - recommendation: одно из 4 значений выше
- strengths: массив из 3 сильных сторон - strengths: массив из 3 сильных сторон
- weaknesses: массив из 3 слабых сторон - weaknesses: массив из 3 слабых сторон
- red_flags: массив из красных флагов (если есть)
""" """
response = openai.chat.completions.create( response = openai.chat.completions.create(
model="gpt-4o-mini", model="gpt-5-mini",
messages=[{"role": "user", "content": evaluation_prompt}], messages=[{"role": "user", "content": evaluation_prompt}],
response_format={"type": "json_object"}, response_format={"type": "json_object"},
temperature=0.3,
) )
evaluation = json.loads(response.choices[0].message.content) evaluation = json.loads(response.choices[0].message.content)
@ -617,6 +619,20 @@ async def _generate_and_upload_pdf_report(
logger.error(f"[PDF_GENERATION] Error generating PDF report: {str(e)}") logger.error(f"[PDF_GENERATION] Error generating PDF report: {str(e)}")
def _format_concerns_field(concerns_data) -> str:
"""Форматирует поле concerns для сохранения как строку"""
if not concerns_data:
return ""
if isinstance(concerns_data, list):
# Если это массив, объединяем элементы через запятую с переносом строки
return "; ".join(concerns_data)
elif isinstance(concerns_data, str):
return concerns_data
else:
return str(concerns_data)
def _create_report_from_dict( def _create_report_from_dict(
interview_session_id: int, report: dict interview_session_id: int, report: dict
) -> "InterviewReport": ) -> "InterviewReport":
@ -633,8 +649,8 @@ def _create_report_from_dict(
technical_skills_justification=scores.get("technical_skills", {}).get( technical_skills_justification=scores.get("technical_skills", {}).get(
"justification", "" "justification", ""
), ),
technical_skills_concerns=scores.get("technical_skills", {}).get( technical_skills_concerns=_format_concerns_field(
"concerns", "" scores.get("technical_skills", {}).get("concerns", "")
), ),
experience_relevance_score=scores.get("experience_relevance", {}).get( experience_relevance_score=scores.get("experience_relevance", {}).get(
"score", 0 "score", 0
@ -642,24 +658,30 @@ def _create_report_from_dict(
experience_relevance_justification=scores.get("experience_relevance", {}).get( experience_relevance_justification=scores.get("experience_relevance", {}).get(
"justification", "" "justification", ""
), ),
experience_relevance_concerns=scores.get("experience_relevance", {}).get( experience_relevance_concerns=_format_concerns_field(
"concerns", "" scores.get("experience_relevance", {}).get("concerns", "")
), ),
communication_score=scores.get("communication", {}).get("score", 0), communication_score=scores.get("communication", {}).get("score", 0),
communication_justification=scores.get("communication", {}).get( communication_justification=scores.get("communication", {}).get(
"justification", "" "justification", ""
), ),
communication_concerns=scores.get("communication", {}).get("concerns", ""), communication_concerns=_format_concerns_field(
scores.get("communication", {}).get("concerns", "")
),
problem_solving_score=scores.get("problem_solving", {}).get("score", 0), problem_solving_score=scores.get("problem_solving", {}).get("score", 0),
problem_solving_justification=scores.get("problem_solving", {}).get( problem_solving_justification=scores.get("problem_solving", {}).get(
"justification", "" "justification", ""
), ),
problem_solving_concerns=scores.get("problem_solving", {}).get("concerns", ""), problem_solving_concerns=_format_concerns_field(
scores.get("problem_solving", {}).get("concerns", "")
),
cultural_fit_score=scores.get("cultural_fit", {}).get("score", 0), cultural_fit_score=scores.get("cultural_fit", {}).get("score", 0),
cultural_fit_justification=scores.get("cultural_fit", {}).get( cultural_fit_justification=scores.get("cultural_fit", {}).get(
"justification", "" "justification", ""
), ),
cultural_fit_concerns=scores.get("cultural_fit", {}).get("concerns", ""), cultural_fit_concerns=_format_concerns_field(
scores.get("cultural_fit", {}).get("concerns", "")
),
# Агрегированные поля # Агрегированные поля
overall_score=report.get("overall_score", 0), overall_score=report.get("overall_score", 0),
recommendation=RecommendationType(report.get("recommendation", "reject")), recommendation=RecommendationType(report.get("recommendation", "reject")),
@ -693,8 +715,8 @@ def _update_report_from_dict(existing_report, report: dict):
existing_report.technical_skills_justification = scores["technical_skills"].get( existing_report.technical_skills_justification = scores["technical_skills"].get(
"justification", "" "justification", ""
) )
existing_report.technical_skills_concerns = scores["technical_skills"].get( existing_report.technical_skills_concerns = _format_concerns_field(
"concerns", "" scores["technical_skills"].get("concerns", "")
) )
if "experience_relevance" in scores: if "experience_relevance" in scores:
@ -704,17 +726,17 @@ def _update_report_from_dict(existing_report, report: dict):
existing_report.experience_relevance_justification = scores[ existing_report.experience_relevance_justification = scores[
"experience_relevance" "experience_relevance"
].get("justification", "") ].get("justification", "")
existing_report.experience_relevance_concerns = scores[ existing_report.experience_relevance_concerns = _format_concerns_field(
"experience_relevance" scores["experience_relevance"].get("concerns", "")
].get("concerns", "") )
if "communication" in scores: if "communication" in scores:
existing_report.communication_score = scores["communication"].get("score", 0) existing_report.communication_score = scores["communication"].get("score", 0)
existing_report.communication_justification = scores["communication"].get( existing_report.communication_justification = scores["communication"].get(
"justification", "" "justification", ""
) )
existing_report.communication_concerns = scores["communication"].get( existing_report.communication_concerns = _format_concerns_field(
"concerns", "" scores["communication"].get("concerns", "")
) )
if "problem_solving" in scores: if "problem_solving" in scores:
@ -724,8 +746,8 @@ def _update_report_from_dict(existing_report, report: dict):
existing_report.problem_solving_justification = scores["problem_solving"].get( existing_report.problem_solving_justification = scores["problem_solving"].get(
"justification", "" "justification", ""
) )
existing_report.problem_solving_concerns = scores["problem_solving"].get( existing_report.problem_solving_concerns = _format_concerns_field(
"concerns", "" scores["problem_solving"].get("concerns", "")
) )
if "cultural_fit" in scores: if "cultural_fit" in scores:
@ -733,8 +755,8 @@ def _update_report_from_dict(existing_report, report: dict):
existing_report.cultural_fit_justification = scores["cultural_fit"].get( existing_report.cultural_fit_justification = scores["cultural_fit"].get(
"justification", "" "justification", ""
) )
existing_report.cultural_fit_concerns = scores["cultural_fit"].get( existing_report.cultural_fit_concerns = _format_concerns_field(
"concerns", "" scores["cultural_fit"].get("concerns", "")
) )
# Агрегированные поля # Агрегированные поля

View File

@ -62,15 +62,15 @@ def generate_interview_plan(
compatibility_prompt = f""" compatibility_prompt = f"""
Проанализируй соответствие кандидата вакансии и определи, стоит ли проводить интервью. Проанализируй соответствие кандидата вакансии и определи, стоит ли проводить интервью.
КЛЮЧЕВОЙ И ЕДИНСТВЕННЫй КРИТЕРИЙ ОТКЛОНЕНИЯ: КЛЮЧЕВЫЕ КРИТЕРИИ ОТКЛОНЕНИЯ:
1. Профессиональная область кандидата: Полное несоответствие сферы деятельности вакансии (иначе 100 за критерий) 1. Несоответствие профессиональной сферы опыт и навыки кандидата не относятся к области деятельности, связанной с вакансией.
ДОПУСТИМЫЕ КРИТЕРИИ: 2. Несоответствие уровня и фокуса позиции текущая или предыдущая должность кандидата существенно отличается по направлению или уровню ответственности от требований вакансии. Допускаются смежные переходы (например, переход из fullstack в frontend или переход кандидата уровня senior на позицию middle/junior).
2. Остальные показатели кандидата хотя бы примерно соответствуют вакансии: скиллы кандидата похожи или смежны вакансионным, опыт не сильно отдален КЛЮЧЕВЫЕ КРИТЕРИИ ДОПУСКА:
от указанного 3. Остальные показатели кандидата примерно соответствуют вакансии: скиллы кандидата похожи или смежны вакансионным, опыт попадает в указанных промежуток
3. Учитывай опыт с аналогичными, похожими, смежными технологиями 4. Учитывай опыт с аналогичными, похожими, смежными технологиями
4. Когда смотришь на вакансию и кандидата не учитывай строгие слова, такие как "Требования", "Ключевые" и тп. Это лишь маркеры, 5. Когда смотришь на вакансию и кандидата не учитывай строгие слова, такие как "Требования", "Ключевые" и тп. Это лишь маркеры,
но не оценочные указатели но не оценочные указатели
5. Если есть спорные вопросы соответствия, лучше допустить к собеседованию и уточнить их там 6. Если есть спорные вопросы соответствия, лучше допустить к собеседованию и уточнить их там
КАНДИДАТ: КАНДИДАТ:
- Имя: {combined_data.get("name", "Не указано")} - Имя: {combined_data.get("name", "Не указано")}
@ -134,7 +134,7 @@ def generate_interview_plan(
# Если кандидат подходит - генерируем план интервью # Если кандидат подходит - генерируем план интервью
plan_prompt = f""" plan_prompt = f"""
Создай детальный план интервью для кандидата на основе его резюме и требований вакансии. Создай детальный план интервью для кандидата на основе его резюме и требований вакансии на 45 МИНУТ.
РЕЗЮМЕ КАНДИДАТА: РЕЗЮМЕ КАНДИДАТА:
- Имя: {combined_data.get("name", "Не указано")} - Имя: {combined_data.get("name", "Не указано")}
@ -182,7 +182,7 @@ def generate_interview_plan(
"Опыт командной работы", "Опыт командной работы",
"Мотивация к изучению нового" "Мотивация к изучению нового"
], ],
"red_flags_to_check": [], "red_flags_to_check": [Шаблонные ответы, уклонения от вопросов, расхождение в стаже],
"personalization_notes": "Кандидат имеет хороший технический опыт" "personalization_notes": "Кандидат имеет хороший технический опыт"
}} }}
""" """