Полуавтоматический отклик на hh.ru: поиск, ИИ-генерация, веб-интерфейс
This commit is contained in:
commit
0ca55f3818
11 files changed
+1740
No files matched your search
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Генерация персонализированного текста отклика через LLM (OpenAI-совместимый API)."""
|
||||
import json
|
||||
import os
|
||||
|
||||
import requests
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def load_env(path=None):
|
||||
"""Простая загрузка .env без внешних зависимостей."""
|
||||
path = path or os.path.join(BASE_DIR, ".env")
|
||||
if not os.path.exists(path):
|
||||
return
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, _, value = line.partition("=")
|
||||
key, value = key.strip(), value.strip().strip('"').strip("'")
|
||||
os.environ.setdefault(key, value)
|
||||
|
||||
|
||||
load_env()
|
||||
|
||||
SYSTEM_PROMPT = """Ты — помощник соискателя на hh.ru. Твоя задача — написать короткое
|
||||
сопроводительное письмо (отклик) на конкретную вакансию от имени соискателя.
|
||||
|
||||
Требования к тексту:
|
||||
- 2-4 предложения, максимум 500 символов (ограничение hh.ru)
|
||||
- Обращение «Здравствуйте!»
|
||||
- Обязательно упомяни название вакансии и компанию
|
||||
- Подчеркни 1-2 релевантных навыка из резюме соискателя, которые подходят под требования вакансии
|
||||
- Без воды, без клише «ответственный, коммуникабельный»
|
||||
- Не выдумывай факты, которых нет в резюме
|
||||
- Только текст письма, без кавычек и пояснений"""
|
||||
|
||||
|
||||
def generate_response(llm_config, vacancy, resume_text, style_hint=""):
|
||||
"""Генерирует текст отклика для вакансии.
|
||||
|
||||
llm_config: {"base_url", "api_key", "model"}
|
||||
vacancy: {"title", "company", "description", "salary", "url"}
|
||||
resume_text: текст резюме соискателя
|
||||
style_hint: необязательные пожелания к стилю
|
||||
"""
|
||||
api_key = llm_config.get("api_key") or os.environ.get(llm_config.get("api_key_env", ""))
|
||||
if not api_key:
|
||||
raise RuntimeError("Не задан API-ключ для LLM (api_key или api_key_env в конфиге)")
|
||||
|
||||
base_url = llm_config["base_url"].rstrip("/")
|
||||
url = f"{base_url}/chat/completions"
|
||||
|
||||
# Урезаем описание вакансии: для генерации отклика достаточно начала
|
||||
description = (vacancy.get('description') or '').strip()
|
||||
if len(description) > 1000:
|
||||
description = description[:1000] + "…"
|
||||
|
||||
user_prompt = f"""ВАКАНСИЯ:
|
||||
Название: {vacancy['title']}
|
||||
Компания: {vacancy.get('company') or 'не указана'}
|
||||
Зарплата: {vacancy.get('salary') or 'не указана'}
|
||||
Описание: {description or 'нет описания'}
|
||||
|
||||
РЕЗЮМЕ СОИСКАТЕЛЯ:
|
||||
{resume_text}
|
||||
|
||||
{'ДОПОЛНИТЕЛЬНЫЕ ПОЖЕЛАНИЯ К СТИЛЮ: ' + style_hint if style_hint else ''}
|
||||
|
||||
Напиши текст отклика."""
|
||||
|
||||
payload = {
|
||||
"model": llm_config["model"],
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_prompt},
|
||||
],
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 300,
|
||||
}
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
resp = requests.post(url, json=payload, headers=headers, timeout=120)
|
||||
if resp.status_code != 200:
|
||||
raise RuntimeError(f"LLM ошибка {resp.status_code}: {resp.text[:300]}")
|
||||
|
||||
data = resp.json()
|
||||
return data["choices"][0]["message"]["content"].strip()
|
||||
|
||||
|
||||
def generate_response_with_retry(llm_config, vacancy, resume_text, style_hint="", attempts=2):
|
||||
"""Генерирует текст с повтором при таймауте/сбое сети."""
|
||||
last_err = None
|
||||
for i in range(attempts):
|
||||
try:
|
||||
return generate_response(llm_config, vacancy, resume_text, style_hint)
|
||||
except Exception as e:
|
||||
last_err = e
|
||||
if i < attempts - 1:
|
||||
print(f" [llm] попытка {i+1} не удалась ({e}), повторяю...")
|
||||
raise last_err
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Быстрый тест: python3 hh_llm.py
|
||||
cfg = {
|
||||
"base_url": os.environ.get("LLM_BASE_URL", "https://api.openai.com/v1"),
|
||||
"api_key_env": "LLM_API_KEY",
|
||||
"model": os.environ.get("LLM_MODEL", "gpt-4o-mini"),
|
||||
}
|
||||
vac = {
|
||||
"title": "Python-разработчик",
|
||||
"company": "ООО Пример",
|
||||
"description": "Разработка бэкенда на Python, Django, PostgreSQL.",
|
||||
"salary": "от 150 000 ₽",
|
||||
}
|
||||
resume = "5 лет опыта в Python, Django, FastAPI, PostgreSQL. Работал в финтехе."
|
||||
print(generate_response(cfg, vac, resume))
|
||||
Reference in new issue
Block a user