upd promts; fix reports
This commit is contained in:
parent
8d449af338
commit
9128bb8881
@ -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,9 +284,17 @@ class InterviewAgent:
|
|||||||
"""Получает информацию о времени интервью"""
|
"""Получает информацию о времени интервью"""
|
||||||
import time
|
import time
|
||||||
|
|
||||||
elapsed_minutes = (time.time() - self.interview_start_time) / 60
|
if self.interview_start_time is None:
|
||||||
remaining_minutes = max(0.0, self.duration_minutes - elapsed_minutes)
|
# Интервью еще не началось
|
||||||
time_percentage = min(100.0, (elapsed_minutes / self.duration_minutes) * 100)
|
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)
|
||||||
|
time_percentage = min(100.0, (elapsed_minutes / self.duration_minutes) * 100)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"elapsed_minutes": elapsed_minutes,
|
"elapsed_minutes": elapsed_minutes,
|
||||||
@ -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")
|
||||||
@ -473,6 +480,18 @@ async def entrypoint(ctx: JobContext):
|
|||||||
return
|
return
|
||||||
|
|
||||||
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(
|
||||||
@ -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
|
||||||
|
@ -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": [
|
||||||
{
|
{
|
||||||
|
@ -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"],
|
||||||
|
@ -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", "")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Агрегированные поля
|
# Агрегированные поля
|
||||||
|
@ -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": "Кандидат имеет хороший технический опыт"
|
||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user