158 lines
7.4 KiB
Python
158 lines
7.4 KiB
Python
import io
|
||
import os
|
||
import traceback
|
||
|
||
from langchain.agents import create_agent
|
||
from langchain_openai import ChatOpenAI
|
||
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
||
from pypdf import PdfReader
|
||
from telegram import (
|
||
InlineKeyboardButton,
|
||
InlineKeyboardMarkup,
|
||
KeyboardButton,
|
||
ReplyKeyboardMarkup,
|
||
Update,
|
||
)
|
||
from telegram.ext import (
|
||
ApplicationBuilder,
|
||
CommandHandler,
|
||
ContextTypes,
|
||
MessageHandler,
|
||
filters,
|
||
)
|
||
|
||
from pydantic import BaseModel
|
||
from typing import Literal
|
||
from vacancies.conf.settings import DB_URI
|
||
from vacancies.main.models import Customer, CustomerCV, JobTitle
|
||
from vacancies.main.vector_store import get_next_vacancy
|
||
|
||
SYSTEM_PROMPT = """
|
||
Ты — карьерный копилот для ИТ. Ты можешь отвечать на любые вопросы по тематике карьеры.
|
||
У тебя есть доступ к резюме пользователя при необходимости.
|
||
Пиши кратко (до 5–6 строк, буллеты приветствуются).
|
||
После полезного ответа предложи что-нибудь, чем ты можешь помочь еще.
|
||
Отвечай простым текстом, не используй форматирование markdown.
|
||
"""
|
||
|
||
|
||
async def get_user_resume(user_id: int):
|
||
"""Получает резюме пользователя для подбора вакансий."""
|
||
customer_cv = await CustomerCV.objects.filter(customer__telegram_id=user_id).afirst()
|
||
return customer_cv.content if customer_cv else ""
|
||
|
||
|
||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
await Customer.objects.aget_or_create(
|
||
telegram_id=update.effective_user.id,
|
||
defaults=dict(
|
||
username=update.effective_user.username,
|
||
chat_id=update.effective_chat.id,
|
||
),
|
||
)
|
||
keyboard = [[KeyboardButton("Получить следующую вакансию")]]
|
||
reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=False)
|
||
text = "Привет! Я карьерный копилот: помогу с работой, интервью и расскажу новости по рынку, специально для тебя. С чего начнем?"
|
||
await context.bot.send_message(chat_id=update.effective_chat.id, text=text, reply_markup=reply_markup)
|
||
|
||
|
||
async def next_vacancy(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
await context.bot.send_message(update.effective_chat.id, "📝 Обрабатываю твой запрос. Пожалуйста, подождите...")
|
||
|
||
customer_cv = await CustomerCV.objects.filter(customer__telegram_id=update.effective_user.id).afirst()
|
||
if not customer_cv:
|
||
message = "Пришлите мне свое резюме, чтобы я мог подобрать вам вакансии!"
|
||
await context.bot.send_message(chat_id=update.effective_chat.id, text=message)
|
||
return
|
||
|
||
vacancy = get_next_vacancy(customer_cv)
|
||
if not vacancy:
|
||
message = "Вакансии закончились, возвращайтесь позже!"
|
||
await context.bot.send_message(chat_id=update.effective_chat.id, text=message)
|
||
return
|
||
|
||
await context.bot.send_message(
|
||
chat_id=update.effective_chat.id,
|
||
text=vacancy.content,
|
||
reply_markup=InlineKeyboardMarkup([[
|
||
InlineKeyboardButton("Откликнуться", url=vacancy.link),
|
||
]]),
|
||
)
|
||
|
||
|
||
async def prompt(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
|
||
agent = create_agent(
|
||
model=ChatOpenAI(model_name="gpt-5-mini", reasoning_effort="minimal"),
|
||
tools=[get_user_resume],
|
||
system_prompt=SYSTEM_PROMPT,
|
||
checkpointer=checkpointer,
|
||
)
|
||
|
||
message = await context.bot.send_message(update.effective_chat.id, "📝 Обрабатываю твой запрос. Пожалуйста, подождите...")
|
||
|
||
response = await agent.ainvoke(
|
||
input={"messages": [{"role": "user", "content": f'user_id = {update.effective_user.id}\n{update.message.text}'}]},
|
||
config={"configurable": {"thread_id": update.effective_user.id}},
|
||
)
|
||
|
||
await context.bot.editMessageText(response['messages'][-1].content, update.effective_chat.id, message.id)
|
||
|
||
|
||
async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||
traceback.print_exception(context.error)
|
||
await context.bot.send_message(chat_id=update.effective_chat.id, text="Произошла ошибка. Повтоите попытку позже.")
|
||
|
||
|
||
async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
message = await context.bot.send_message(update.effective_chat.id, "📝 Обрабатываю твой запрос. Пожалуйста, подождите...")
|
||
|
||
if not update.message.document:
|
||
await context.bot.send_message(chat_id=update.effective_chat.id, text="Не удалось прочитать информацию из файла! Попробуйте другой формат.")
|
||
return
|
||
|
||
buffer = io.BytesIO()
|
||
file = await update.message.document.get_file()
|
||
await file.download_to_memory(buffer)
|
||
reader = PdfReader(buffer)
|
||
resume = "\n".join(page.extract_text() for page in reader.pages)
|
||
|
||
job_titles = JobTitle.objects.values_list('title', flat=True)
|
||
job_title_map = dict(JobTitle.objects.values_list('title', 'id'))
|
||
|
||
class Structure(BaseModel):
|
||
job_title: Literal[tuple(job_titles)]
|
||
min_salary_rub: int | None
|
||
max_salary_rub: int | None
|
||
|
||
openai_client = ChatOpenAI(model_name="gpt-5-mini", temperature=0, seed=42, top_p=1)
|
||
structured_llm = openai_client.with_structured_output(Structure)
|
||
|
||
prompt = f"""
|
||
Ты — HR-классификатор. Ниже приведён список допустимых профессий.
|
||
Твоя задача — выбрать наиболее подходящую по смыслу.
|
||
Качество классификации - самое важное.
|
||
Игнорируй орфографические и стилистические различия.
|
||
Резюме:
|
||
{resume}
|
||
"""
|
||
response = await structured_llm.ainvoke(prompt)
|
||
|
||
customer = await Customer.objects.aget(telegram_id=update.effective_user.id)
|
||
customer_cv, _ = await CustomerCV.objects.aupdate_or_create(customer=customer, defaults=dict(
|
||
content=resume,
|
||
job_title_id=job_title_map[response.job_title],
|
||
min_salary_rub=response.min_salary_rub,
|
||
max_salary_rub=response.max_salary_rub,
|
||
))
|
||
|
||
await context.bot.editMessageText("Отлично! Запомнил Ваше резюме.", update.effective_chat.id, message.id)
|
||
|
||
|
||
application = ApplicationBuilder().token(os.environ["BOT_TOKEN"]).concurrent_updates(True).build()
|
||
application.add_handler(CommandHandler('start', start, block=False))
|
||
application.add_handler(MessageHandler(filters.Text("Получить следующую вакансию"), next_vacancy, block=False))
|
||
application.add_handler(MessageHandler(filters.TEXT & (~filters.COMMAND), prompt, block=False))
|
||
application.add_handler(MessageHandler((filters.Document.ALL | filters.PHOTO) & (~filters.COMMAND), handle_document, block=False))
|
||
application.add_error_handler(error_handler)
|