import asyncio import io import os import traceback from typing import Literal from asgiref.sync import sync_to_async from langchain.agents import create_agent from langchain_openai import ChatOpenAI from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver from pydantic import BaseModel from pypdf import PdfReader from telegram import ( InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup, Update, ) from telegram.ext import ( ApplicationBuilder, CommandHandler, ContextTypes, MessageHandler, filters, ) from vacancies.conf.settings import DB_URI from vacancies.main.models import Customer, CustomerCV, JobTitle from vacancies.main.recommendations 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 = await asyncio.to_thread(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, parse_mode="Markdown", text=vacancy.get_formatted_response(), 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) db_job_titles = await sync_to_async(list)(JobTitle.objects.values_list('title', flat=True)) job_title_map = await sync_to_async(dict)(JobTitle.objects.values_list('title', 'id')) class Structure(BaseModel): job_titles: list[Literal[tuple(db_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, min_salary_rub=response.min_salary_rub, max_salary_rub=response.max_salary_rub, )) await customer_cv.job_titles.aset([job_title_map[job_title] for job_title in response.job_titles]) 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)