Init project
This commit is contained in:
commit
a5e6fba87c
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
|
||||
```
|
||||
48
backend/agent.py
Normal file
48
backend/agent.py
Normal 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()}.
|
||||
Ты — карьерный копилот для ИТ.
|
||||
Ответ всегда должен быть, не допускай пустых ответов.
|
||||
Требования к ответам:
|
||||
- Пиши кратко (до 5–6 строк, буллеты приветствуются).
|
||||
- Всегда проверяй факты: бери данные о вакансиях только из контекста и ссылок в web-поиске.
|
||||
- В ответ всегда давай источники (минимум 1, лучше 2–3): ссылка на публичный
|
||||
- Вакансии можно брать только из контекста, либо обращаться к 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
60
backend/bot.py
Normal 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
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