Compare commits

..

2 Commits

Author SHA1 Message Date
b224ef29d3 Improve vacancy rendering in messages
All checks were successful
release / docker (push) Successful in 38s
2025-11-09 23:41:52 +03:00
6ff25281e2 Order recommendations by timestamp 2025-11-09 22:52:05 +03:00
6 changed files with 74 additions and 19 deletions

View File

@ -1,12 +1,14 @@
import asyncio
import io import io
import os import os
import asyncio
import traceback import traceback
from typing import Literal
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from langchain.agents import create_agent from langchain.agents import create_agent
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from pydantic import BaseModel
from pypdf import PdfReader from pypdf import PdfReader
from telegram import ( from telegram import (
InlineKeyboardButton, InlineKeyboardButton,
@ -23,8 +25,6 @@ from telegram.ext import (
filters, filters,
) )
from pydantic import BaseModel
from typing import Literal
from vacancies.conf.settings import DB_URI from vacancies.conf.settings import DB_URI
from vacancies.main.models import Customer, CustomerCV, JobTitle from vacancies.main.models import Customer, CustomerCV, JobTitle
from vacancies.main.recommendations import get_next_vacancy from vacancies.main.recommendations import get_next_vacancy
@ -59,7 +59,7 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
async def next_vacancy(update: Update, context: ContextTypes.DEFAULT_TYPE): async def next_vacancy(update: Update, context: ContextTypes.DEFAULT_TYPE):
await context.bot.send_message(update.effective_chat.id, "📝 Обрабатываю твой запрос. Пожалуйста, подождите...") await context.bot.send_message(update.effective_chat.id, " Обрабатываю твой запрос. Пожалуйста, подождите...")
customer_cv = await CustomerCV.objects.filter(customer__telegram_id=update.effective_user.id).afirst() customer_cv = await CustomerCV.objects.filter(customer__telegram_id=update.effective_user.id).afirst()
if not customer_cv: if not customer_cv:
@ -75,7 +75,8 @@ async def next_vacancy(update: Update, context: ContextTypes.DEFAULT_TYPE):
await context.bot.send_message( await context.bot.send_message(
chat_id=update.effective_chat.id, chat_id=update.effective_chat.id,
text=vacancy.content, parse_mode="Markdown",
text=vacancy.get_formatted_response(),
reply_markup=InlineKeyboardMarkup([[ reply_markup=InlineKeyboardMarkup([[
InlineKeyboardButton("Откликнуться", url=vacancy.link), InlineKeyboardButton("Откликнуться", url=vacancy.link),
]]), ]]),
@ -91,7 +92,7 @@ async def prompt(update: Update, context: ContextTypes.DEFAULT_TYPE):
checkpointer=checkpointer, checkpointer=checkpointer,
) )
message = await context.bot.send_message(update.effective_chat.id, "📝 Обрабатываю твой запрос. Пожалуйста, подождите...") message = await context.bot.send_message(update.effective_chat.id, " Обрабатываю твой запрос. Пожалуйста, подождите...")
response = await agent.ainvoke( response = await agent.ainvoke(
input={"messages": [{"role": "user", "content": f'user_id = {update.effective_user.id}\n{update.message.text}'}]}, input={"messages": [{"role": "user", "content": f'user_id = {update.effective_user.id}\n{update.message.text}'}]},
@ -107,7 +108,7 @@ async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> N
async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE): async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE):
message = await context.bot.send_message(update.effective_chat.id, "📝 Обрабатываю твой запрос. Пожалуйста, подождите...") message = await context.bot.send_message(update.effective_chat.id, " Обрабатываю твой запрос. Пожалуйста, подождите...")
if not update.message.document: if not update.message.document:
await context.bot.send_message(chat_id=update.effective_chat.id, text="Не удалось прочитать информацию из файла! Попробуйте другой формат.") await context.bot.send_message(chat_id=update.effective_chat.id, text="Не удалось прочитать информацию из файла! Попробуйте другой формат.")

View File

@ -1,14 +1,15 @@
from itertools import batched
from datetime import timedelta from datetime import timedelta
from django.utils import timezone from itertools import batched
from pydantic import BaseModel
from typing import Literal from typing import Literal
from vacancies.main.models import Vacancy, JobTitle
from langchain_openai import ChatOpenAI
import clickhouse_connect import clickhouse_connect
from django.core.management import BaseCommand
from django.conf import settings from django.conf import settings
from django.core.management import BaseCommand
from django.utils import timezone
from langchain_openai import ChatOpenAI
from pydantic import BaseModel
from vacancies.main.models import JobTitle, Vacancy
query = """ query = """
SELECT DISTINCT ON (message) id, chat_username, telegram_id, message, timestamp SELECT DISTINCT ON (message) id, chat_username, telegram_id, message, timestamp
@ -42,6 +43,8 @@ class Command(BaseCommand):
job_title: Literal[tuple(job_titles)] job_title: Literal[tuple(job_titles)]
min_salary_rub: int | None min_salary_rub: int | None
max_salary_rub: int | None max_salary_rub: int | None
company_name: str
requirements: str
openai_client = ChatOpenAI(model_name="gpt-5-mini", temperature=0, seed=42, top_p=1) openai_client = ChatOpenAI(model_name="gpt-5-mini", temperature=0, seed=42, top_p=1)
structured_llm = openai_client.with_structured_output(Structure) structured_llm = openai_client.with_structured_output(Structure)
@ -76,6 +79,8 @@ class Command(BaseCommand):
job_title_id=job_title_map[response.job_title], job_title_id=job_title_map[response.job_title],
min_salary_rub=response.min_salary_rub, min_salary_rub=response.min_salary_rub,
max_salary_rub=response.max_salary_rub, max_salary_rub=response.max_salary_rub,
company_name=response.company_name,
requirements=response.requirements,
content=message, content=message,
timestamp=timezone.make_aware(timestamp), timestamp=timezone.make_aware(timestamp),
link=f"https://t.me/{chat_username}/{telegram_id}", link=f"https://t.me/{chat_username}/{telegram_id}",

View File

@ -1,11 +1,12 @@
import asyncio import asyncio
from django.core.management import BaseCommand from django.core.management import BaseCommand
from vacancies.main.models import CustomerCV
from vacancies.main.bot import application
from vacancies.main.recommendations import get_next_vacancy
from telegram import InlineKeyboardButton, InlineKeyboardMarkup from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from vacancies.main.bot import application
from vacancies.main.models import CustomerCV
from vacancies.main.recommendations import get_next_vacancy
class Command(BaseCommand): class Command(BaseCommand):
help = "Generates new recommended vacancies" help = "Generates new recommended vacancies"
@ -18,7 +19,8 @@ class Command(BaseCommand):
if vacancy := get_next_vacancy(customer_cv): if vacancy := get_next_vacancy(customer_cv):
await application.bot.send_message( await application.bot.send_message(
chat_id=customer_cv.customer.chat_id, chat_id=customer_cv.customer.chat_id,
text=vacancy.content, text=vacancy.get_formatted_response(),
parse_mode="Markdown",
reply_markup=InlineKeyboardMarkup([[ reply_markup=InlineKeyboardMarkup([[
InlineKeyboardButton("Откликнуться", url=vacancy.link), InlineKeyboardButton("Откликнуться", url=vacancy.link),
]]), ]]),

View File

@ -0,0 +1,25 @@
# Generated by Django 5.2.7 on 2025-11-09 19:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0011_remove_customercv_job_title_customercv_job_titles_and_more'),
]
operations = [
migrations.AddField(
model_name='vacancy',
name='company_name',
field=models.CharField(default='test', max_length=255),
preserve_default=False,
),
migrations.AddField(
model_name='vacancy',
name='requirements',
field=models.TextField(default='test'),
preserve_default=False,
),
]

View File

@ -45,6 +45,8 @@ class Vacancy(models.Model):
external_id = models.CharField(max_length=255, unique=True) external_id = models.CharField(max_length=255, unique=True)
min_salary_rub = models.PositiveIntegerField(null=True, blank=True, default=None) min_salary_rub = models.PositiveIntegerField(null=True, blank=True, default=None)
max_salary_rub = models.PositiveIntegerField(null=True, blank=True, default=None) max_salary_rub = models.PositiveIntegerField(null=True, blank=True, default=None)
company_name = models.CharField(max_length=255)
requirements = models.TextField()
content = models.TextField() content = models.TextField()
timestamp = models.DateTimeField() timestamp = models.DateTimeField()
link = models.URLField() link = models.URLField()
@ -52,6 +54,25 @@ class Vacancy(models.Model):
def __str__(self): def __str__(self):
return self.job_title.title return self.job_title.title
def get_formatted_response(self):
response = f"""
💼 **Вакансия**: {self.job_title}
\n🏢 **Компания**: {self.company_name}
\n📝 **Требования**: {self.requirements}
"""
if self.min_salary_rub:
if self.max_salary_rub:
response += f"\n💸 **ЗП**: от {self.min_salary_rub} т.р."
else:
response += f"\n💸 **ЗП**: {self.min_salary_rub} т.р. - {self.max_salary_rub} т.р."
elif self.max_salary_rub:
response += f"\n💸 **ЗП**: до {self.max_salary_rub} т.р."
return response
class Meta:
verbose_name_plural = 'Vacancies'
class RecommendedVacancy(models.Model): class RecommendedVacancy(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="recommended_vacancies") customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="recommended_vacancies")

View File

@ -1,13 +1,14 @@
from vacancies.main.models import Vacancy
from django.db.models import Q from django.db.models import Q
from vacancies.main.models import Vacancy
def get_next_vacancy(customer_cv): def get_next_vacancy(customer_cv):
vacancy = Vacancy.objects.filter( vacancy = Vacancy.objects.filter(
~Q(id__in=customer_cv.customer.recommended_vacancies.values_list("vacancy_id", flat=True)), ~Q(id__in=customer_cv.customer.recommended_vacancies.values_list("vacancy_id", flat=True)),
Q(min_salary_rub__isnull=True) | Q(min_salary_rub__gt=customer_cv.min_salary_rub), Q(min_salary_rub__isnull=True) | Q(min_salary_rub__gt=customer_cv.min_salary_rub),
job_title__title__in=customer_cv.job_titles.values_list("title", flat=True), job_title__title__in=customer_cv.job_titles.values_list("title", flat=True),
).first() ).order_by("-timestamp").first()
if vacancy: if vacancy:
customer_cv.customer.recommended_vacancies.create(vacancy=vacancy) customer_cv.customer.recommended_vacancies.create(vacancy=vacancy)