Init project
This commit is contained in:
commit
0204093de0
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
|
.env
|
||||||
|
*.db
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
19
README.md
Normal file
19
README.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# vision-career-backend
|
||||||
|
|
||||||
|
Sample `.env`:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
DEEPINFRA_API_TOKEN=your-token-here
|
||||||
|
OPENAI_API_KEY=your-token-here
|
||||||
|
|
||||||
|
BOT_TOKEN=your-token-here
|
||||||
|
|
||||||
|
SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||||
|
```
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=$(pwd) uv run --env-file .env backend/index.py
|
||||||
|
PYTHONPATH=$(pwd) uv run --env-file .env backend/bot.py
|
||||||
|
```
|
||||||
49
backend/agent.py
Normal file
49
backend/agent.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
from langchain.agents import create_agent
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
from langgraph.checkpoint.memory import InMemorySaver
|
||||||
|
from langchain_community.tools import DuckDuckGoSearchRun
|
||||||
|
from langchain_community.vectorstores import Clickhouse, ClickhouseSettings
|
||||||
|
from langchain_community.embeddings import DeepInfraEmbeddings
|
||||||
|
from datetime import datetime
|
||||||
|
from redis.asyncio import Redis
|
||||||
|
|
||||||
|
SYSTEM_PROMPT = f"""
|
||||||
|
Сегодня {datetime.today().isoformat()}.
|
||||||
|
Ты — карьерный копилот для ИТ.
|
||||||
|
Ответ всегда должен быть, не допускай пустых ответов.
|
||||||
|
Требования к ответам:
|
||||||
|
- Пиши кратко (до 5–6 строк, буллеты приветствуются).
|
||||||
|
- Всегда проверяй факты: бери данные о вакансиях только из контекста и ссылок в web-поиске.
|
||||||
|
- В ответ всегда давай источники (минимум 1, лучше 2–3): ссылка на публичный
|
||||||
|
- Вакансии можно брать только из контекста, либо обращаться к web-поиску.
|
||||||
|
- Всегда указывай дату вакансии и ссылку на нее.
|
||||||
|
- Если вакансия из контекста, то сформируй ссылку на telegram-сообщение, где была указана эта вакансия.
|
||||||
|
пост/новость/страницу компании. Без «уверенностей».
|
||||||
|
- Персональные/идентифицирующие данные из непубличных источников не
|
||||||
|
раскрывай. Если цитируешь, то только из публичных каналов/страниц с ссылкой.
|
||||||
|
- Если данных недостаточно: честно скажи «не хватает надёжных источников»,
|
||||||
|
предложи расширить период/переформулировать, либо выполнить веб-поиск.
|
||||||
|
- После полезного ответа предложи один мягкий следующий шаг.
|
||||||
|
Отвечай простым текстом. Не используй HTML, Markdown, звёздочки, подчёркивания. Просто текст, но используй emoji.
|
||||||
|
"""
|
||||||
|
|
||||||
|
redis = Redis()
|
||||||
|
llm = ChatOpenAI(model_name="zai-org/GLM-4.6", openai_api_base="https://api.deepinfra.com/v1/openai")
|
||||||
|
embedding = DeepInfraEmbeddings(model_id="Qwen/Qwen3-Embedding-8B")
|
||||||
|
vectorstore = Clickhouse(embedding, ClickhouseSettings(port=8123, username="default", password="", index_type="vector_similarity"))
|
||||||
|
search_tool = DuckDuckGoSearchRun()
|
||||||
|
|
||||||
|
async def get_relevant_vacancies(requirements: str):
|
||||||
|
"""Получает релевантные вакансии из базы данных по переданным требованиям."""
|
||||||
|
return await vectorstore.asimilarity_search(requirements, k=10)
|
||||||
|
|
||||||
|
async def get_user_resume(user_id: int):
|
||||||
|
"""Получает резюме пользователя для подбора вакансий."""
|
||||||
|
return await redis.get(user_id)
|
||||||
|
|
||||||
|
agent = create_agent(
|
||||||
|
model=llm,
|
||||||
|
tools=[search_tool, get_relevant_vacancies, get_user_resume],
|
||||||
|
system_prompt=SYSTEM_PROMPT,
|
||||||
|
checkpointer=InMemorySaver(),
|
||||||
|
)
|
||||||
72
backend/bot.py
Normal file
72
backend/bot.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import os
|
||||||
|
import io
|
||||||
|
import traceback
|
||||||
|
from telegram import Update, ReplyKeyboardMarkup
|
||||||
|
from telegram.ext import filters, ApplicationBuilder, MessageHandler, CommandHandler, ContextTypes
|
||||||
|
from backend.agent import agent, redis
|
||||||
|
from pypdf import PdfReader
|
||||||
|
|
||||||
|
VACANCIES = "👥 Вакансии"
|
||||||
|
RESUME = "📄 Резюме"
|
||||||
|
|
||||||
|
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
text = "Привет! Я карьерный копилот: помогу с работой, интервью и расскажу новости по рынку, специально для тебя. С чего начнем?"
|
||||||
|
reply_markup = ReplyKeyboardMarkup([[VACANCIES, RESUME]], resize_keyboard=True, one_time_keyboard=False)
|
||||||
|
await context.bot.send_message(chat_id=update.effective_chat.id, text=text, reply_markup=reply_markup)
|
||||||
|
|
||||||
|
|
||||||
|
async def prompt(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
if update.message.text == RESUME:
|
||||||
|
resume = await redis.get(update.effective_user.id)
|
||||||
|
text = "Пришли мне файл с твоим резюме, чтобы я смог найти подходящие вакансии на основе твоих компетенций!"
|
||||||
|
if resume:
|
||||||
|
text = "Я уже получил твое резюме. Если хочешь его обновить, пришли мне новый файл!"
|
||||||
|
await context.bot.send_message(update.effective_chat.id, text)
|
||||||
|
return
|
||||||
|
|
||||||
|
user_prompt = update.message.text
|
||||||
|
if update.message.text == VACANCIES:
|
||||||
|
user_prompt = "Пришли мне актуальные вакансии на основе моего резюме"
|
||||||
|
|
||||||
|
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{user_prompt}"}]},
|
||||||
|
config={"configurable": {"thread_id": "1"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
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:
|
||||||
|
print(traceback.format_exception(context.error)[-1])
|
||||||
|
await context.bot.send_message(chat_id=update.effective_chat.id, text="Произошла ошибка. Повтоите попытку позже.")
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
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)
|
||||||
|
await redis.set(update.effective_user.id, resume)
|
||||||
|
|
||||||
|
await context.bot.send_message(chat_id=update.effective_chat.id, text="Отлично! Запомнил Ваше резюме.")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
application = ApplicationBuilder().token(os.environ["BOT_TOKEN"]).build()
|
||||||
|
start_handler = CommandHandler('start', start)
|
||||||
|
application.add_handler(start_handler)
|
||||||
|
application.add_handler(MessageHandler(filters.TEXT & (~filters.COMMAND), prompt))
|
||||||
|
application.add_handler(MessageHandler((filters.Document.ALL | filters.PHOTO) & (~filters.COMMAND), handle_document))
|
||||||
|
application.add_error_handler(error_handler)
|
||||||
|
application.run_polling()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
27
backend/index.py
Normal file
27
backend/index.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from backend.agent import vectorstore
|
||||||
|
from langchain_core.documents import Document
|
||||||
|
import clickhouse_connect
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT id, chat_id, telegram_id, message, timestamp
|
||||||
|
FROM telegram_parser_chatmessage
|
||||||
|
WHERE timestamp >= now() - INTERVAL 30 DAY
|
||||||
|
AND length(message) > 150
|
||||||
|
AND arrayCount(x -> position(message, x) > 0, [
|
||||||
|
'вакансия', 'ищем', 'требуется', 'разработчик', 'будет плюсом',
|
||||||
|
'зарплата', 'оклад', 'з/п', 'руб', 'опыт работы',
|
||||||
|
'требования', 'обязанности', 'условия', 'компания', 'офис',
|
||||||
|
'удаленно', 'гибкий график', 'полный день', 'частичная занятость',
|
||||||
|
'резюме', 'собеседование', 'junior', 'middle', 'senior'
|
||||||
|
]) >= 5
|
||||||
|
"""
|
||||||
|
|
||||||
|
client = clickhouse_connect.create_client(port=18123)
|
||||||
|
|
||||||
|
documents = []
|
||||||
|
for row in client.query(query).result_rows:
|
||||||
|
(id, chat_id, telegram_id, message, timestamp) = row
|
||||||
|
metadata = {"chat_id": chat_id, "telegram_id": telegram_id, "timestamp": timestamp.isoformat()}
|
||||||
|
documents.append(Document(id=id, page_content=message, metadata=metadata))
|
||||||
|
|
||||||
|
vectorstore.add_documents(documents)
|
||||||
20
compose.yaml
Normal file
20
compose.yaml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
services:
|
||||||
|
clickhouse:
|
||||||
|
image: clickhouse:25.7.5.34-jammy
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
CLICKHOUSE_PASSWORD: ""
|
||||||
|
CLICKHOUSE_DB: default
|
||||||
|
CLICKHOUSE_USER: default
|
||||||
|
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1"
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8123:8123"
|
||||||
|
- "127.0.0.1:9000:9000"
|
||||||
|
volumes:
|
||||||
|
- /srv/vision-career/clickhouse/ch_data:/var/lib/clickhouse/
|
||||||
|
- /srv/vision-career/clickhouse/ch_logs:/var/log/clickhouse-server/
|
||||||
|
valkey:
|
||||||
|
image: valkey/valkey:latest
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:6379:6379"
|
||||||
20
pyproject.toml
Normal file
20
pyproject.toml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
[project]
|
||||||
|
name = "vision-career-backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"clickhouse-connect>=0.9.2",
|
||||||
|
"ddgs>=9.6.1",
|
||||||
|
"deepinfra>=0.1.0",
|
||||||
|
"langchain>=0.3.27",
|
||||||
|
"langchain-community>=0.3.31",
|
||||||
|
"langchain-openai>=0.3.35",
|
||||||
|
"pypdf>=6.1.2",
|
||||||
|
"python-telegram-bot>=22.5",
|
||||||
|
"redis>=6.4.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
langchain-community = { git = "https://github.com/estromenko/langchain-community", subdirectory = "libs/community", rev = "feat/add-vector-similarity-index-support" }
|
||||||
Loading…
Reference in New Issue
Block a user