feat: add AI-powered cover letter generation and improve modal handling
- Add automatic cover letter generation using Gemini AI - Implement smart detection of "Add cover letter" button in modals - Generate personalized cover letters based on real resume data and vacancy text - Improve modal window detection with updated selectors for HH.ru - Fix code formatting to comply with black and flake8 standards - Add .flake8 configuration with modern standards (88 char limit) - Handle cover letter field detection and auto-filling - Graceful fallback to default letter if AI generation fails Features: - Smart cover letter button detection - Personalized content generation via Gemini AI - Robust error handling for cover letter functionality - Maintains existing functionality without cover letters
This commit is contained in:
parent
fd8da44b84
commit
5973d0f70e
|
@ -0,0 +1,11 @@
|
|||
[flake8]
|
||||
max-line-length = 88
|
||||
extend-ignore = E203, W503
|
||||
exclude =
|
||||
.git,
|
||||
__pycache__,
|
||||
.pytest_cache,
|
||||
venv,
|
||||
env,
|
||||
.venv,
|
||||
.env
|
|
@ -85,7 +85,7 @@ class CLIInterface:
|
|||
print(f"📝 Всего заявок: {stats['total_applications']}")
|
||||
print(f"✅ Успешных: {stats['successful']}")
|
||||
print(f"❌ Неудачных: {stats['failed']}")
|
||||
|
||||
|
||||
if "skipped" in stats and stats["skipped"] > 0:
|
||||
print(f"⏭️ Пропущено: {stats['skipped']}")
|
||||
|
||||
|
|
|
@ -197,7 +197,7 @@ class Settings:
|
|||
return bool(self.gemini.api_key)
|
||||
|
||||
def get_exclude_keywords(self) -> list:
|
||||
return ['стажер', 'cv']
|
||||
return ["стажер", "cv"]
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
|
|
@ -131,7 +131,9 @@ class AutomationOrchestrator:
|
|||
def _apply_to_vacancies(self, vacancies: List[Vacancy]) -> List[ApplicationResult]:
|
||||
max_successful_apps = settings.application.max_applications
|
||||
|
||||
logger.info(f"📨 ЭТАП 4: Подача заявок (максимум {max_successful_apps} успешных)")
|
||||
logger.info(
|
||||
f"📨 ЭТАП 4: Подача заявок (максимум {max_successful_apps} успешных)"
|
||||
)
|
||||
logger.info("💡 Между заявками добавляются паузы")
|
||||
logger.info("💡 Лимит считается только по успешным заявкам")
|
||||
|
||||
|
@ -141,27 +143,34 @@ class AutomationOrchestrator:
|
|||
|
||||
for vacancy in vacancies:
|
||||
if successful_count >= max_successful_apps:
|
||||
logger.info(f"🎯 Достигнут лимит успешных заявок: {max_successful_apps}")
|
||||
logger.info(
|
||||
f"🎯 Достигнут лимит успешных заявок: {max_successful_apps}"
|
||||
)
|
||||
break
|
||||
|
||||
processed_count += 1
|
||||
truncated_name = UIFormatter.truncate_text(vacancy.name, medium=True)
|
||||
logger.info(
|
||||
f"Обработка {processed_count}: {truncated_name} (успешных: {successful_count}/{max_successful_apps})"
|
||||
f"Обработка {processed_count}: {truncated_name} "
|
||||
f"(успешных: {successful_count}/{max_successful_apps})"
|
||||
)
|
||||
|
||||
try:
|
||||
result = self.browser_service.apply_to_vacancy(
|
||||
vacancy.alternate_url, vacancy.name
|
||||
)
|
||||
result = self.browser_service.apply_to_vacancy(vacancy)
|
||||
application_results.append(result)
|
||||
self._log_application_result(result)
|
||||
|
||||
if result.success:
|
||||
successful_count += 1
|
||||
logger.info(f" 🎉 Успешных заявок: {successful_count}/{max_successful_apps}")
|
||||
logger.info(
|
||||
f" 🎉 Успешных заявок: "
|
||||
f"{successful_count}/{max_successful_apps}"
|
||||
)
|
||||
|
||||
if processed_count < len(vacancies) and successful_count < max_successful_apps:
|
||||
if (
|
||||
processed_count < len(vacancies)
|
||||
and successful_count < max_successful_apps
|
||||
):
|
||||
self.browser_service.add_random_pause()
|
||||
|
||||
except Exception as e:
|
||||
|
@ -174,7 +183,10 @@ class AutomationOrchestrator:
|
|||
)
|
||||
application_results.append(error_result)
|
||||
|
||||
logger.info(f"🏁 Обработка завершена. Обработано вакансий: {processed_count}, успешных заявок: {successful_count}")
|
||||
logger.info(
|
||||
f"🏁 Обработка завершена. Обработано вакансий: {processed_count}, "
|
||||
f"успешных заявок: {successful_count}"
|
||||
)
|
||||
return application_results
|
||||
|
||||
def _log_search_results(
|
||||
|
|
|
@ -15,13 +15,15 @@ from selenium.common.exceptions import NoSuchElementException
|
|||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
|
||||
from ..config.settings import settings, AppConstants, UIFormatter
|
||||
from ..models.vacancy import ApplicationResult
|
||||
from ..models.vacancy import ApplicationResult, Vacancy
|
||||
|
||||
|
||||
class SubmissionResult(Enum):
|
||||
SUCCESS = "success"
|
||||
FAILED = "failed"
|
||||
FAILED = "failed"
|
||||
SKIPPED = "skipped"
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -44,7 +46,7 @@ class SessionManager:
|
|||
"timestamp": time.time(),
|
||||
}
|
||||
|
||||
with open(self.cookies_file, 'w', encoding='utf-8') as f:
|
||||
with open(self.cookies_file, "w", encoding="utf-8") as f:
|
||||
json.dump(session_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
logger.info(f"Сессия сохранена в {self.cookies_file}")
|
||||
|
@ -61,7 +63,7 @@ class SessionManager:
|
|||
logger.info("Файл сессии не найден")
|
||||
return False
|
||||
|
||||
with open(self.cookies_file, 'r', encoding='utf-8') as f:
|
||||
with open(self.cookies_file, "r", encoding="utf-8") as f:
|
||||
session_data = json.load(f)
|
||||
|
||||
if not self._is_session_valid(session_data):
|
||||
|
@ -167,11 +169,11 @@ class AuthenticationHandler:
|
|||
try:
|
||||
print("\n🔐 ПРОВЕРКА СОХРАНЕННОЙ СЕССИИ")
|
||||
logger.info("Проверяем сохраненную сессию...")
|
||||
|
||||
|
||||
if self.session_manager.load_session():
|
||||
self.driver.refresh()
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
if self._check_authentication():
|
||||
print("✅ Использована сохраненная сессия")
|
||||
logger.info("Авторизация через сохраненную сессию успешна!")
|
||||
|
@ -193,12 +195,12 @@ class AuthenticationHandler:
|
|||
|
||||
if self._check_authentication():
|
||||
logger.info("Авторизация успешна!")
|
||||
|
||||
|
||||
if self.session_manager.save_session():
|
||||
print("✅ Сессия сохранена для следующих запусков")
|
||||
else:
|
||||
print("⚠️ Не удалось сохранить сессию")
|
||||
|
||||
|
||||
return True
|
||||
else:
|
||||
logger.error("Авторизация не завершена")
|
||||
|
@ -250,21 +252,19 @@ class VacancyApplicator:
|
|||
def __init__(self, driver: webdriver.Chrome):
|
||||
self.driver = driver
|
||||
|
||||
def apply_to_vacancy(
|
||||
self, vacancy_url: str, vacancy_name: str
|
||||
) -> ApplicationResult:
|
||||
def apply_to_vacancy(self, vacancy: Vacancy) -> ApplicationResult:
|
||||
"""Подача заявки на вакансию"""
|
||||
try:
|
||||
truncated_name = UIFormatter.truncate_text(vacancy_name)
|
||||
truncated_name = UIFormatter.truncate_text(vacancy.name)
|
||||
logger.info(f"Переход к вакансии: {truncated_name}...")
|
||||
self.driver.get(vacancy_url)
|
||||
self.driver.get(vacancy.alternate_url)
|
||||
time.sleep(3)
|
||||
|
||||
apply_button = self._find_apply_button()
|
||||
if not apply_button:
|
||||
return ApplicationResult(
|
||||
vacancy_id="",
|
||||
vacancy_name=vacancy_name,
|
||||
vacancy_name=vacancy.name,
|
||||
success=False,
|
||||
error_message="Кнопка отклика не найдена",
|
||||
)
|
||||
|
@ -273,7 +273,7 @@ class VacancyApplicator:
|
|||
if self._is_already_applied(button_text):
|
||||
return ApplicationResult(
|
||||
vacancy_id="",
|
||||
vacancy_name=vacancy_name,
|
||||
vacancy_name=vacancy.name,
|
||||
success=False,
|
||||
already_applied=True,
|
||||
error_message="Уже откликались на эту вакансию",
|
||||
|
@ -283,28 +283,30 @@ class VacancyApplicator:
|
|||
time.sleep(2)
|
||||
|
||||
logger.info("Кнопка отклика нажата, ищем форму заявки...")
|
||||
|
||||
submit_result = self._submit_application_form()
|
||||
|
||||
|
||||
submit_result = self._submit_application_form(vacancy)
|
||||
|
||||
if submit_result == SubmissionResult.SUCCESS:
|
||||
logger.info("✅ Заявка успешно отправлена")
|
||||
return ApplicationResult(
|
||||
vacancy_id="", vacancy_name=vacancy_name, success=True
|
||||
vacancy_id="", vacancy_name=vacancy.name, success=True
|
||||
)
|
||||
elif submit_result == SubmissionResult.SKIPPED:
|
||||
logger.warning("⚠️ Вакансия пропущена (нет модального окна)")
|
||||
return ApplicationResult(
|
||||
vacancy_id="",
|
||||
vacancy_name=vacancy_name,
|
||||
vacancy_name=vacancy.name,
|
||||
success=False,
|
||||
skipped=True,
|
||||
error_message="Модальное окно не найдено (возможно тестовая вакансия)",
|
||||
error_message=(
|
||||
"Модальное окно не найдено " "(возможно тестовая вакансия)"
|
||||
),
|
||||
)
|
||||
else: # FAILED
|
||||
logger.warning("❌ Не удалось отправить заявку в модальном окне")
|
||||
return ApplicationResult(
|
||||
vacancy_id="",
|
||||
vacancy_name=vacancy_name,
|
||||
vacancy_name=vacancy.name,
|
||||
success=False,
|
||||
error_message="Ошибка отправки в модальном окне",
|
||||
)
|
||||
|
@ -313,7 +315,7 @@ class VacancyApplicator:
|
|||
logger.error(f"Ошибка при подаче заявки: {e}")
|
||||
return ApplicationResult(
|
||||
vacancy_id="",
|
||||
vacancy_name=vacancy_name,
|
||||
vacancy_name=vacancy.name,
|
||||
success=False,
|
||||
error_message=str(e),
|
||||
)
|
||||
|
@ -334,19 +336,19 @@ class VacancyApplicator:
|
|||
indicator in button_text for indicator in self.ALREADY_APPLIED_INDICATORS
|
||||
)
|
||||
|
||||
def _submit_application_form(self) -> SubmissionResult:
|
||||
def _submit_application_form(self, vacancy: Vacancy) -> SubmissionResult:
|
||||
"""Отправка заявки в модальном окне"""
|
||||
try:
|
||||
modal_selectors = [
|
||||
'[data-qa="modal-overlay"]',
|
||||
'.magritte-modal-overlay',
|
||||
".magritte-modal-overlay",
|
||||
'[data-qa="modal"]',
|
||||
'[data-qa="vacancy-response-popup"]',
|
||||
'.vacancy-response-popup',
|
||||
'.modal',
|
||||
'.bloko-modal',
|
||||
".vacancy-response-popup",
|
||||
".modal",
|
||||
".bloko-modal",
|
||||
]
|
||||
|
||||
|
||||
submit_selectors = [
|
||||
'[data-qa="vacancy-response-submit-popup"]',
|
||||
'button[form="RESPONSE_MODAL_FORM_ID"]',
|
||||
|
@ -372,7 +374,10 @@ class VacancyApplicator:
|
|||
continue
|
||||
|
||||
if not modal_found:
|
||||
logger.warning("⚠️ Модальное окно не найдено - пропускаем вакансию (возможно тестовая или ошибка)")
|
||||
logger.warning(
|
||||
"⚠️ Модальное окно не найдено - пропускаем вакансию "
|
||||
"(возможно тестовая или ошибка)"
|
||||
)
|
||||
return SubmissionResult.SKIPPED
|
||||
|
||||
form_selectors = [
|
||||
|
@ -380,7 +385,7 @@ class VacancyApplicator:
|
|||
'form[id="RESPONSE_MODAL_FORM_ID"]',
|
||||
'form[data-qa*="response"]',
|
||||
]
|
||||
|
||||
|
||||
form_found = False
|
||||
for form_selector in form_selectors:
|
||||
try:
|
||||
|
@ -391,11 +396,12 @@ class VacancyApplicator:
|
|||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
|
||||
if not form_found:
|
||||
logger.warning("Форма отклика не найдена в модальном окне - пропускаем")
|
||||
return SubmissionResult.SKIPPED
|
||||
|
||||
self._add_cover_letter_if_possible(vacancy)
|
||||
time.sleep(1)
|
||||
|
||||
for selector in submit_selectors:
|
||||
|
@ -404,8 +410,13 @@ class VacancyApplicator:
|
|||
EC.element_to_be_clickable((By.CSS_SELECTOR, selector))
|
||||
)
|
||||
if submit_button:
|
||||
logger.info(f"Нажимаем кнопку отправки: {submit_button.text.strip()}")
|
||||
self.driver.execute_script("arguments[0].click();", submit_button)
|
||||
logger.info(
|
||||
f"Нажимаем кнопку отправки: "
|
||||
f"{submit_button.text.strip()}"
|
||||
)
|
||||
self.driver.execute_script(
|
||||
"arguments[0].click();", submit_button
|
||||
)
|
||||
time.sleep(3)
|
||||
if self._check_success_message():
|
||||
return SubmissionResult.SUCCESS
|
||||
|
@ -421,12 +432,93 @@ class VacancyApplicator:
|
|||
logger.error(f"Ошибка в модальном окне: {e}")
|
||||
return SubmissionResult.FAILED
|
||||
|
||||
def _add_cover_letter_if_possible(self, vacancy: Vacancy) -> None:
|
||||
"""Добавление сопроводительного письма если возможно"""
|
||||
try:
|
||||
cover_letter_button_selectors = [
|
||||
'[data-qa="add-cover-letter"]',
|
||||
'button[data-qa*="cover-letter"]',
|
||||
'button:contains("Добавить сопроводительное")',
|
||||
'button:contains("сопроводительное")',
|
||||
]
|
||||
|
||||
cover_letter_button = None
|
||||
for selector in cover_letter_button_selectors:
|
||||
try:
|
||||
if selector.startswith("button:contains"):
|
||||
buttons = self.driver.find_elements(By.TAG_NAME, "button")
|
||||
text_to_find = selector.split('"')[1].lower()
|
||||
for button in buttons:
|
||||
if text_to_find in button.text.lower():
|
||||
cover_letter_button = button
|
||||
break
|
||||
else:
|
||||
cover_letter_button = self.driver.find_element(
|
||||
By.CSS_SELECTOR, selector
|
||||
)
|
||||
|
||||
if cover_letter_button:
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not cover_letter_button:
|
||||
logger.info("Кнопка сопроводительного письма не найдена")
|
||||
return
|
||||
|
||||
logger.info("Найдена кнопка сопроводительного письма, нажимаем...")
|
||||
self.driver.execute_script("arguments[0].click();", cover_letter_button)
|
||||
time.sleep(2)
|
||||
|
||||
cover_letter_field_selectors = [
|
||||
'textarea[data-qa*="cover-letter"]',
|
||||
'textarea[name*="letter"]',
|
||||
'textarea[placeholder*="письм"]',
|
||||
'textarea[id*="letter"]',
|
||||
".modal textarea",
|
||||
"form textarea",
|
||||
]
|
||||
|
||||
cover_letter_field = None
|
||||
for selector in cover_letter_field_selectors:
|
||||
try:
|
||||
cover_letter_field = WebDriverWait(self.driver, 3).until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, selector))
|
||||
)
|
||||
if cover_letter_field:
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not cover_letter_field:
|
||||
logger.warning("Поле для сопроводительного письма не найдено")
|
||||
return
|
||||
|
||||
logger.info("Генерация сопроводительного письма...")
|
||||
|
||||
from ..services.gemini_service import GeminiAIService
|
||||
|
||||
gemini_service = GeminiAIService()
|
||||
cover_letter_text = gemini_service.generate_cover_letter(vacancy)
|
||||
|
||||
if cover_letter_text:
|
||||
logger.info("Заполняем сопроводительное письмо...")
|
||||
cover_letter_field.clear()
|
||||
cover_letter_field.send_keys(cover_letter_text)
|
||||
logger.info("✅ Сопроводительное письмо добавлено")
|
||||
else:
|
||||
logger.warning("Не удалось сгенерировать сопроводительное письмо")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Ошибка при добавлении сопроводительного письма: {e}")
|
||||
logger.info("Продолжаем без сопроводительного письма")
|
||||
|
||||
def _check_success_message(self) -> bool:
|
||||
"""Проверка успешной отправки заявки"""
|
||||
try:
|
||||
success_indicators = [
|
||||
"отклик отправлен",
|
||||
"заявка отправлена",
|
||||
"заявка отправлена",
|
||||
"успешно отправлено",
|
||||
"спасибо за отклик",
|
||||
"ваш отклик получен",
|
||||
|
@ -435,50 +527,59 @@ class VacancyApplicator:
|
|||
"резюме отправлено",
|
||||
"откликнулись на вакансию",
|
||||
]
|
||||
|
||||
|
||||
success_selectors = [
|
||||
'[data-qa*="success"]',
|
||||
'[data-qa*="sent"]',
|
||||
'.success-message',
|
||||
'.response-sent',
|
||||
".success-message",
|
||||
".response-sent",
|
||||
'[class*="success"]',
|
||||
]
|
||||
|
||||
|
||||
for selector in success_selectors:
|
||||
try:
|
||||
success_element = self.driver.find_element(By.CSS_SELECTOR, selector)
|
||||
success_element = self.driver.find_element(
|
||||
By.CSS_SELECTOR, selector
|
||||
)
|
||||
if success_element and success_element.is_displayed():
|
||||
logger.info(f"Найден элемент успеха: {selector} - {success_element.text}")
|
||||
logger.info(
|
||||
f"Найден элемент успеха: {selector} - "
|
||||
f"{success_element.text}"
|
||||
)
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
|
||||
page_text = self.driver.page_source.lower()
|
||||
|
||||
|
||||
for indicator in success_indicators:
|
||||
if indicator in page_text:
|
||||
logger.info(f"Найдено подтверждение: '{indicator}'")
|
||||
return True
|
||||
|
||||
|
||||
current_url = self.driver.current_url
|
||||
if "sent" in current_url or "success" in current_url or "response" in current_url:
|
||||
if (
|
||||
"sent" in current_url
|
||||
or "success" in current_url
|
||||
or "response" in current_url
|
||||
):
|
||||
logger.info(f"URL указывает на успешную отправку: {current_url}")
|
||||
return True
|
||||
|
||||
|
||||
modal_disappeared = True
|
||||
try:
|
||||
self.driver.find_element(By.CSS_SELECTOR, '[data-qa="modal-overlay"]')
|
||||
modal_disappeared = False
|
||||
except NoSuchElementException:
|
||||
pass
|
||||
|
||||
|
||||
if modal_disappeared:
|
||||
logger.info("Модальное окно исчезло - возможно отклик отправлен")
|
||||
return True
|
||||
|
||||
|
||||
logger.info("Подтверждение отправки не найдено")
|
||||
return False
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки успеха: {e}")
|
||||
return False
|
||||
|
@ -531,19 +632,17 @@ class BrowserService:
|
|||
self._is_authenticated = True
|
||||
return success
|
||||
|
||||
def apply_to_vacancy(
|
||||
self, vacancy_url: str, vacancy_name: str
|
||||
) -> ApplicationResult:
|
||||
def apply_to_vacancy(self, vacancy: Vacancy) -> ApplicationResult:
|
||||
"""Подача заявки на вакансию"""
|
||||
if not self.is_ready():
|
||||
return ApplicationResult(
|
||||
vacancy_id="",
|
||||
vacancy_name=vacancy_name,
|
||||
vacancy_name=vacancy.name,
|
||||
success=False,
|
||||
error_message="Браузер не готов или нет авторизации",
|
||||
)
|
||||
|
||||
return self.applicator.apply_to_vacancy(vacancy_url, vacancy_name)
|
||||
return self.applicator.apply_to_vacancy(vacancy)
|
||||
|
||||
def add_random_pause(self) -> None:
|
||||
"""Случайная пауза между действиями"""
|
||||
|
|
|
@ -84,8 +84,14 @@ class GeminiApiClient:
|
|||
json_str = content[json_start:json_end]
|
||||
parsed_response = json.loads(json_str)
|
||||
|
||||
score = parsed_response.get("match_score", 0)
|
||||
logger.info(f"Gemini анализ завершен: {score}")
|
||||
if "match_score" in parsed_response:
|
||||
score = parsed_response.get("match_score", 0)
|
||||
logger.info(f"Gemini анализ завершен: {score}")
|
||||
elif "cover_letter" in parsed_response:
|
||||
logger.info("Gemini сгенерировал сопроводительное письмо")
|
||||
else:
|
||||
logger.info("Получен ответ от Gemini")
|
||||
|
||||
return parsed_response
|
||||
else:
|
||||
logger.error("JSON не найден в ответе Gemini")
|
||||
|
@ -330,3 +336,88 @@ class GeminiAIService:
|
|||
return score >= settings.gemini.match_threshold
|
||||
|
||||
return self.analyzer.should_apply(vacancy)
|
||||
|
||||
def generate_cover_letter(self, vacancy: Vacancy) -> Optional[str]:
|
||||
"""Генерация сопроводительного письма для вакансии"""
|
||||
if not self.is_available():
|
||||
logger.warning("Gemini API недоступен, используем базовое письмо")
|
||||
return self._get_default_cover_letter()
|
||||
|
||||
try:
|
||||
resume_data = self.resume_loader.load()
|
||||
vacancy_text = self._get_vacancy_full_text(vacancy)
|
||||
|
||||
experience_text = resume_data.get("experience", "")
|
||||
about_me_text = resume_data.get("about_me", "")
|
||||
skills_text = resume_data.get("skills", "")
|
||||
|
||||
my_profile = f"""
|
||||
Опыт работы:
|
||||
{experience_text}
|
||||
|
||||
О себе:
|
||||
{about_me_text}
|
||||
|
||||
Навыки и технологии:
|
||||
{skills_text}
|
||||
"""
|
||||
|
||||
prompt_text = (
|
||||
"Напиши короткое, человечное и честное сопроводительное письмо "
|
||||
"для отклика на вакансию на русском языке. Не придумывай опыт, "
|
||||
"которого нет. Используй только мой реальный опыт и навыки ниже. "
|
||||
"Пиши по делу, дружелюбно и без официоза. Не делай письмо слишком "
|
||||
"длинным. Всегда заканчивай строкой «Telegram — @itqen»."
|
||||
)
|
||||
|
||||
prompt = f"""{prompt_text}
|
||||
|
||||
**Верни только JSON с ключом "cover_letter", без других пояснений.**
|
||||
|
||||
Пример формата вывода:
|
||||
{{"cover_letter": "текст письма здесь"}}
|
||||
|
||||
**Вот мой опыт:**
|
||||
{my_profile}
|
||||
|
||||
**Вот текст вакансии:**
|
||||
{vacancy_text}"""
|
||||
|
||||
logger.info("Генерация сопроводительного письма через Gemini")
|
||||
response = self.api_client.generate_content(prompt)
|
||||
|
||||
if response and "cover_letter" in response:
|
||||
cover_letter = response["cover_letter"]
|
||||
logger.info("Сопроводительное письмо сгенерировано")
|
||||
return cover_letter
|
||||
else:
|
||||
logger.error("Не удалось получить сопроводительное письмо от Gemini")
|
||||
return self._get_default_cover_letter()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка генерации сопроводительного письма: {e}")
|
||||
return self._get_default_cover_letter()
|
||||
|
||||
def _get_vacancy_full_text(self, vacancy: Vacancy) -> str:
|
||||
"""Получение полного текста вакансии"""
|
||||
parts = [
|
||||
f"Название: {vacancy.name}",
|
||||
f"Компания: {vacancy.employer.name}",
|
||||
]
|
||||
|
||||
if vacancy.snippet.requirement:
|
||||
parts.append(f"Требования: {vacancy.snippet.requirement}")
|
||||
|
||||
if vacancy.snippet.responsibility:
|
||||
parts.append(f"Обязанности: {vacancy.snippet.responsibility}")
|
||||
|
||||
return "\n\n".join(parts)
|
||||
|
||||
def _get_default_cover_letter(self) -> str:
|
||||
"""Базовое сопроводительное письмо на случай ошибки"""
|
||||
return """Добрый день!
|
||||
|
||||
Заинтересован в данной вакансии. Готов обсудить детали и возможности сотрудничества.
|
||||
|
||||
С уважением,
|
||||
Telegram — @itqen"""
|
||||
|
|
Loading…
Reference in New Issue