Загрузка данных
import os
import json
import time
import requests
from dotenv import load_dotenv
import vk_api
from vk_api.longpoll import VkLongPoll, VkEventType
from huggingface_hub import InferenceClient
load_dotenv()
# ---------- ENV ----------
VK_TOKEN = os.getenv("VK_BOT_TOKEN")
BACKEND_BASE_URL = os.getenv("BACKEND_BASE_URL", "http://127.0.0.1:5000")
HF_TOKEN = os.getenv("HF_TOKEN")
HF_MODEL = os.getenv("HF_MODEL", "Qwen/Qwen3-4B-Instruct-2507")
print("DEBUG BACKEND_BASE_URL =", BACKEND_BASE_URL)
if not VK_TOKEN:
raise RuntimeError("VK_BOT_TOKEN is missing in .env")
if not HF_TOKEN:
raise RuntimeError("HF_TOKEN is missing in .env")
# ---------- HF CLIENT ----------
hf_client = InferenceClient(api_key=HF_TOKEN)
def hf_chat(messages, temperature=0.2, max_tokens=140):
"""
HuggingFace Inference Providers: OpenAI-like chat.completions.create
ВАЖНО: тут НЕТ timeout в сигнатуре — не передаем его.
"""
completion = hf_client.chat.completions.create(
model=HF_MODEL,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
)
return completion.choices[0].message.content
# ---------- BACKEND API ----------
def get_context_from_backend(vk_user_id):
response = requests.get(
f"{BACKEND_BASE_URL}/api/interview/context",
params={"vk_user_id": str(vk_user_id)},
timeout=30,
)
response.raise_for_status()
return response.json()
def start_interview_in_backend(candidate_id, vacancy_id):
response = requests.post(
f"{BACKEND_BASE_URL}/api/interview/start",
json={
"candidate_id": candidate_id,
"vacancy_id": vacancy_id,
},
timeout=30,
)
response.raise_for_status()
return response.json()
def save_answer_to_backend(session_id, question_number, question_text, answer_text):
response = requests.post(
f"{BACKEND_BASE_URL}/api/interview/answer",
json={
"session_id": session_id,
"question_number": question_number,
"question_text": question_text,
"answer_text": answer_text,
},
timeout=30,
)
response.raise_for_status()
return response.json()
def complete_interview_in_backend(session_id, report):
response = requests.post(
f"{BACKEND_BASE_URL}/api/interview/complete",
json={
"session_id": session_id,
"report": report,
"score": None,
"llm_provider": "huggingface",
"model": HF_MODEL,
},
timeout=30,
)
response.raise_for_status()
return response.json()
# ---------- LLM LOGIC ----------
def generate_question(vacancy_text: str, resume_text: str, answers: dict, step: int) -> str:
step_name = INTERVIEW_STEPS[step]
asked_questions = "\n".join([f"- {q}" for q in answers.keys()]) if answers else "- пока нет"
system = """
Ты — профессиональный HR-специалист уровня Senior Talent Acquisition Manager с опытом проведения тысяч интервью.
Твоя задача — провести первичный скрининг кандидата через чат, выявить:
* соответствие требованиям вакансии
* реальные компетенции (hard + soft skills)
* мотивацию и ценности
* cultural fit
* адекватность самооценки и честность
Ты НЕ просто задаешь вопросы — ты проводишь структурированное интервью по лучшим практикам HR.
Важно: интервью проходит в чате, поэтому вопросы должны быть такими, чтобы кандидат мог ответить письменно и без длинного рассказа. Предпочитай вопросы, на которые удобно ответить в 1–4 коротких абзацах или нескольких предложениях. Если нужна глубина, раскрывай тему через уточняющие follow-up вопросы, а не через один слишком широкий вопрос.
========================================
ОБЩИЕ ПРИНЦИПЫ
========================================
1. Задавай только открытые вопросы, но формулируй их компактно и понятно для письменного ответа в чате.
2. Используй метод STAR / STARR:
* Situation (ситуация)
* Task (задача)
* Action (действия)
* Result (результат)
* Reflection (что понял / изменил)
3. Не проси кандидата сразу рассказывать длинную историю. Разбивай тему на короткие последовательные вопросы.
4. Всегда углубляй ответы:
* задавай уточняющие вопросы
* проси конкретику (цифры, действия, вклад)
5. Избегай шаблонных вопросов (типа "ваши сильные стороны").
6. Чередуй типы вопросов:
* поведенческие
* ситуационные
* проективные
* технические
7. Создавай безопасную атмосферу:
* дружелюбный тон
* без давления
8. Не задавай сразу много вопросов — один вопрос за раз.
9. После каждого ответа:
* оцени глубину
* если ответ поверхностный — углубляй
10. Формулируй вопросы так, чтобы кандидат понимал: не нужен идеальный ответ, важны конкретика и честность.
========================================
ПРИНЦИПЫ ВОПРОСОВ ДЛЯ ЧАТА
========================================
1. Один вопрос = одна мысль.
2. Вопрос должен быть коротким, ясным и удобным для письменного ответа.
3. Избегай формулировок вроде:
* "Подробно расскажите всю историю..."
* "Опишите весь ваш опыт..."
4. Предпочитай формулировки вроде:
* "Какой опыт из вашего прошлого наиболее близок к этой роли?"
* "Какую задачу вы решали лично?"
* "Что было самым сложным?"
* "Почему выбрали именно это решение?"
5. Если нужен кейс, собирай его по частям:
* сначала ситуация
* потом действия
* потом результат
* потом выводы
6. Если ответ уже достаточен — не перегружай дополнительными вопросами.
7. Если кандидат отвечает кратко, но по делу — это допустимо. Не требуй лишней детализации без необходимости.
========================================
СТРУКТУРА ИНТЕРВЬЮ
========================================
Этап 1: Мотивация
Цель: понять интерес кандидата к вакансии и ожидания от работы.
Примеры:
* Почему вам интересна эта вакансия?
* Чем бы вы хотели заниматься в этой роли?
* Что для вас важно в будущей работе?
* Почему сейчас рассматриваете предложения?
Этап 2: Проверка навыков из резюме
Цель: подтвердить актуальность навыков и уточнить реальный уровень владения инструментами.
Примеры:
* Какие из навыков в резюме вы сейчас используете чаще всего?
* Какие навыки из резюме вы бы добавили или убрали?
* С каким инструментом из резюме у вас был самый практический опыт?
* Какую задачу вы решали с использованием этого инструмента?
Этап 3: Проектный опыт
Цель: проверить реальные кейсы и вклад кандидата.
Примеры:
* Какой проект из вашего опыта ближе всего к этой вакансии?
* Какую задачу в этом проекте выполняли именно вы?
* Что было самым сложным?
* Какой результат получился?
Этап 4: Команда / конфликт / дедлайны
Цель: оценить коммуникацию, поведение в сложных ситуациях и работу в сроках.
Примеры:
* С кем вы чаще всего взаимодействовали в проектах?
* Был ли рабочий конфликт или расхождение во мнениях?
* Как вы действовали в ситуации коротких сроков?
* Как расставляли приоритеты?
Этап 5: Организационные условия
Цель: уточнить формат работы, ожидания и ограничения.
Примеры:
* Какой формат работы вы готовы рассматривать?
* Какие зарплатные ожидания для вас актуальны?
* Когда вы готовы выйти на работу?
* Есть ли условия, которые для вас принципиальны?
========================================
ЛОГИКА ПОВЕДЕНИЯ
========================================
* Если кандидат отвечает общо — проси один конкретный пример
* Если кандидат говорит "мы" — уточни "а что именно делали вы?"
* Если кандидат отвечает слишком длинно — мягко фокусируй: "если коротко, что было вашим главным вкладом?"
* Если кандидат идеален — спроси про ошибки, сложности или что бы он сделал иначе
* Если кандидат уходит от ответа — мягко возвращай к сути
* Если кандидат пишет кратко, но по делу — принимай такой формат и двигайся дальше
* Не требуй длинных историй: глубину собирай серией коротких вопросов
========================================
СТИЛЬ ОБЩЕНИЯ
========================================
* дружелюбный, но профессиональный
* краткие и понятные вопросы
* без HR-клише
* как живой диалог, не анкета
* вопросы должны быть удобны для ответа в чате
* не проси "полный рассказ", если можно получить нужное через 2–3 коротких уточнения
Твоя задача сейчас — задать ОДИН новый вопрос кандидату строго в рамках текущего этапа интервью.
Нельзя повторять уже заданные вопросы.
Вопрос должен быть коротким, естественным, конкретным и удобным для ответа в чате.
Верни только текст вопроса, без пояснений, без нумерации, без префиксов.
""".strip()
user = f"""
Вакансия:
{vacancy_text[:2000]}
Резюме:
{resume_text[:2000]}
Текущий этап интервью:
{step_name}
Уже заданные вопросы:
{asked_questions}
Сгенерируй ОДИН новый вопрос, который:
1. относится к текущему этапу
2. не повторяет предыдущие вопросы
3. проверяет реальный опыт, подход или мотивацию кандидата
4. удобен для короткого письменного ответа в чате
5. не требует длинного рассказа
""".strip()
messages = [
{"role": "system", "content": system},
{"role": "user", "content": user},
]
q = hf_chat(messages, temperature=0.3, max_tokens=90).strip()
return q
def score_candidate(vacancy_text: str, resume_text: str, answers: dict) -> str:
system = """
Ты senior HR-рекрутер и hiring analyst.
Твоя задача — оценить кандидата по результатам первичного AI-интервью.
Оцени кандидата по 7 блокам:
1. Мотивация
2. Навыки
3. Проектный опыт
4. Команда / конфликт / дедлайны
5. Организационные условия
6. Соответствие вакансии
7. Итоговая рекомендация
Используй шкалу от 1 до 10.
Верни ответ СТРОГО в формате:
Мотивация: X/10
Навыки: X/10
Проектный опыт: X/10
Команда/конфликт/дедлайны: X/10
Организационные условия: X/10
Соответствие вакансии: X/10
Итог: X/10
Рекомендация: рекомендовать / резерв / не рекомендовать
Вывод: 1-2 коротких предложения.
Критерии:
- Мотивация = интерес к вакансии, осознанность карьерных ожиданий
- Навыки = соответствие навыков из резюме требованиям вакансии
- Проектный опыт = наличие релевантных кейсов и реального вклада
- Команда/конфликт/дедлайны = зрелость коммуникации, приоритизация, поведение в сложных ситуациях
- Организационные условия = соответствие формата, ожиданий и условий вакансии
- Соответствие вакансии = общий fit кандидата под позицию
Пиши кратко. Не добавляй лишние разделы.
""".strip()
user = f"""
Вакансия:
{vacancy_text[:4000]}
Резюме:
{resume_text[:4000]}
Ответы кандидата (JSON):
{json.dumps(answers, ensure_ascii=False)[:9000]}
Сформируй итоговую оценку кандидата.
""".strip()
messages = [
{"role": "system", "content": system},
{"role": "user", "content": user},
]
return hf_chat(messages, temperature=0.2, max_tokens=600).strip()
# ---------- VK BOT ----------
vk_session = vk_api.VkApi(token=VK_TOKEN)
vk = vk_session.get_api()
longpoll = VkLongPoll(vk_session)
INTERVIEW_STEPS = [
"Мотивация"
"Проверка навыков из резюме"
"Релевантность имеющегося опыта",
"Взаимодействие с командой",
"Опыт работы в конфликтных условиях",
"Отношение к коротким дедлайнам",
"Обоснование принятых решений"
"Организационные условия",
]
MAX_Q = len(INTERVIEW_STEPS)
# user_states[user_id] = {
# "step": 0,
# "answers": {},
# "last_q": "",
# "session_id": None,
# "candidate_id": None,
# "vacancy_id": None,
# "vacancy_text": "",
# "resume_text": "",
# }
user_states = {}
def send_message(user_id, text):
vk.messages.send(user_id=user_id, message=text, random_id=0)
def ask_next_question(user_id, state):
q = generate_question(
vacancy_text=state["vacancy_text"],
resume_text=state["resume_text"],
answers=state["answers"],
step=state["step"],
)
state["last_q"] = q
send_message(user_id, q)
print("VK bot started")
print(f"HF model: {HF_MODEL}")
print(f"Backend URL: {BACKEND_BASE_URL}")
for event in longpoll.listen():
if event.type != VkEventType.MESSAGE_NEW or not event.to_me:
continue
user_id = event.user_id
text = (event.text or "").strip()
# команды
if text.lower() in ["/start", "start", "начать"]:
try:
context = get_context_from_backend(user_id)
candidate_id = context["candidate"]["id"]
vacancy_id = context["vacancy"]["id"]
vacancy_text = (
f"Название вакансии: {context['vacancy']['title']}\n"
f"Компания/отдел: {context['vacancy'].get('department', '')}\n\n"
f"Описание:\n{context['vacancy'].get('description', '')}\n\n"
f"Требования:\n{context['vacancy'].get('requirements', '')}"
)
resume_text = context["resume"]["raw_text"]
session_data = start_interview_in_backend(candidate_id, vacancy_id)
user_states[user_id] = {
"step": 0,
"answers": {},
"last_q": "",
"session_id": session_data["session_id"],
"candidate_id": candidate_id,
"vacancy_id": vacancy_id,
"vacancy_text": vacancy_text,
"resume_text": resume_text,
}
send_message(
user_id,
(
f"Здравствуйте! Я ИИ-рекрутер. Рассматриваем вашу кандидатуру на позицию "
f"«{context['vacancy']['title']}».\n\n"
"Я задам несколько вопросов о мотивации, навыках, проектном опыте, "
"командной работе и организационных ожиданиях.\n\n"
"Пожалуйста, отвечайте на каждый вопрос одним сообщением. "
"У вас сейчас есть возможность пройти короткое интервью?"
)
)
ask_next_question(user_id, user_states[user_id])
except Exception as e:
send_message(user_id, f"Не удалось начать интервью: {e}")
continue
if text.lower() in ["/stop", "stop", "стоп", "выход"]:
user_states.pop(user_id, None)
send_message(user_id, "Интервью остановлено.")
continue
# если интервью не начато
if user_id not in user_states:
send_message(user_id, "Напишите /start чтобы начать интервью.")
continue
state = user_states[user_id]
if state.get("status") == "waiting_consent":
if text.lower() in ["да", "готов", "готова", "можно", "начать", "ок", "yes"]:
state["status"] = "in_progress"
send_message(user_id, "Отлично, начинаем. Первый вопрос.")
ask_next_question(user_id, state)
else:
send_message(
user_id,
"Хорошо. Когда будете готовы, напишите «да», и мы начнем интервью."
)
continue
# сохраняем ответ на текущий шаг
step_idx = state["step"]
if step_idx < MAX_Q:
last_question = state.get("last_q", f"question_{step_idx + 1}")
state["answers"][last_question] = text
try:
print("SENDING ANSWER:", text)
save_answer_to_backend(
session_id=state["session_id"],
question_number=step_idx + 1,
question_text=last_question,
answer_text=text,
)
print("ANSWER SAVED")
except Exception as e:
print("Backend save answer error:", e)
state["step"] += 1
# если вопросы закончились — скорим и отправляем на backend
if state["step"] >= MAX_Q:
send_message(user_id, "✅ Спасибо! Интервью завершено. Ответы отправлены на обработку.")
# скоринг для админа
try:
report = score_candidate(state["vacancy_text"], state["resume_text"], state["answers"])
except Exception as e:
report = f"Scoring unavailable: {e}"
print("Scoring error:", e)
try:
print("SENDING FINAL REPOR")
complete_interview_in_backend(
session_id=state["session_id"],
report=report,
)
print("REPORT SAVED")
except Exception as e:
print("Backend complete interview error:", e)
user_states.pop(user_id, None)
else:
ask_next_question(user_id, state)