Init project

This commit is contained in:
estromenko 2025-10-19 12:14:21 +03:00
commit a5e6fba87c
9 changed files with 2021 additions and 0 deletions

12
.gitignore vendored Normal file
View 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
View File

@ -0,0 +1 @@
3.13

19
README.md Normal file
View 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
```

48
backend/agent.py Normal file
View File

@ -0,0 +1,48 @@
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()}.
Ты карьерный копилот для ИТ.
Ответ всегда должен быть, не допускай пустых ответов.
Требования к ответам:
- Пиши кратко (до 56 строк, буллеты приветствуются).
- Всегда проверяй факты: бери данные о вакансиях только из контекста и ссылок в web-поиске.
- В ответ всегда давай источники (минимум 1, лучше 23): ссылка на публичный
- Вакансии можно брать только из контекста, либо обращаться к web-поиску.
- Всегда указывай дату вакансии и ссылку на нее.
- Если вакансия из контекста, то сформируй ссылку на telegram-сообщение, где была указана эта вакансия.
пост/новость/страницу компании. Без «уверенностей».
- Персональные/идентифицирующие данные из непубличных источников не
раскрывай. Если цитируешь, то только из публичных каналов/страниц с ссылкой.
- Если данных недостаточно: честно скажи «не хватает надёжных источников»,
предложи расширить период/переформулировать, либо выполнить веб-поиск.
- После полезного ответа предложи один мягкий следующий шаг
"""
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-0.6B")
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(),
)

60
backend/bot.py Normal file
View File

@ -0,0 +1,60 @@
import os
import re
import io
import traceback
from telegram import Update
from telegram.constants import ParseMode
from telegram.ext import filters, ApplicationBuilder, MessageHandler, CommandHandler, ContextTypes
from backend.agent import agent, redis
from pypdf import PdfReader
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
text = "Привет! Я карьерный копилот: помогу с работой, интервью и расскажу новости по рынку, специально для тебя. С чего начнем?"
await context.bot.send_message(chat_id=update.effective_chat.id, text=text)
async def prompt(update: Update, context: ContextTypes.DEFAULT_TYPE):
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": "1"}},
)
llm_message = re.sub(f'([{re.escape(r'_*[]()~`>#+-=|{}.!')}])', r'\\\1', response['messages'][-1].content)
await context.bot.editMessageText(llm_message, update.effective_chat.id, message.id, parse_mode=ParseMode.MARKDOWN_V2)
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
View 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
View 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
View 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" }

1814
uv.lock Normal file

File diff suppressed because it is too large Load Diff