127 lines
6.5 KiB
Python
127 lines
6.5 KiB
Python
import os
|
||
import io
|
||
import traceback
|
||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update, ReplyKeyboardMarkup, KeyboardButton
|
||
from telegram.ext import filters, ApplicationBuilder, MessageHandler, CommandHandler, ContextTypes
|
||
from pypdf import PdfReader
|
||
from vacancies.main.models import Customer, CustomerCV
|
||
from langchain.agents import create_agent
|
||
from langchain_openai import ChatOpenAI
|
||
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
||
from vacancies.main.vector_store import add_vectors, extract_features, get_next_vacancy
|
||
from vacancies.conf.settings import DB_URI
|
||
|
||
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
|
||
|
||
result = get_next_vacancy(customer_cv)
|
||
if not result:
|
||
message = "Вакансии закончились, возвращайтесь позже!"
|
||
await context.bot.send_message(chat_id=update.effective_chat.id, text=message)
|
||
return
|
||
|
||
recommendation, vacancy_content, link = result
|
||
|
||
await context.bot.send_message(
|
||
chat_id=update.effective_chat.id,
|
||
text=vacancy_content,
|
||
reply_markup=InlineKeyboardMarkup([[
|
||
InlineKeyboardButton("Откликнуться", url=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)
|
||
|
||
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,
|
||
))
|
||
features = extract_features(customer_cv.content)
|
||
add_vectors(
|
||
"cvs",
|
||
customer_cv.id,
|
||
features.model_dump(),
|
||
{'content': customer_cv.content, 'features_json': features.model_dump()},
|
||
)
|
||
|
||
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)
|