#!/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))