Files
hh_auto/hh_llm.py
T

126 lines
5.3 KiB
Python

#!/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()
content = (data["choices"][0]["message"].get("content") or "").strip()
if not content:
raise RuntimeError("LLM вернул пустой текст — попробуйте ещё раз")
return content
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))