126 lines
5.3 KiB
Python
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)) |