
Салют! Меня зовут Григорий, я главный по спецпроектам в AllSee. Если вы когда‑нибудь серьёзно подходили к вопросу поиска работы, то вам определённо приходилось муторно писать сопроводительные письма под каждую вакансию. В данной статье я расскажу, как можно автоматизировать составление релевантного для вакансии сопроводительного письма с учётом вашего резюме.
Какие вводные?
В процессе поиска работы приходится писать индивидуальные сопроводительные письма для каждой вакансии по отдельности. Я хочу автоматизировать данный процесс, генерируя сопроводительное письмо с учётом текста моего резюме и конкретной вакансии, а также иметь возможность сгенерировать его на основе шаблона. Для генерации я буду использовать YandexGPT API, а для обработки входящих запросов — FastAPI.
Routines (служебные функции)
Скрытый текст
Чтение YAML и JSON
def read_yaml(path: str) -> dict: with open(path, 'r') as stream: return yaml.safe_load(stream) def read_json(path: str) -> dict: with open(path, 'r') as stream: return json.load(stream)
YandexGPT API
Перейдём сразу к коду для отправки запросов на генерацию:
async def send_completion_request( self, messages: List[Dict[str, Any]], temperature: float = 0.6, max_tokens: int = 1000, stream: bool = False, completion_url: str = "https://llm.api.cloud.yandex.net/foundationModels/v1/completion" ) -> Dict[str, Any]: # checking config manager if not all([ getattr(self.config_manager, 'model_type', None), getattr(self.config_manager, 'iam_token', None), getattr(self.config_manager, 'catalog_id', None) ]): raise ValueError("Model type, IAM token, and catalog ID must be set in config manager to send a " "completion request.") # making request headers: Dict[str, str] = { "Content-Type": "application/json", "Authorization": f"Bearer {self.config_manager.iam_token}", "x-folder-id": self.config_manager.catalog_id } data: Dict[str, Any] = { "modelUri": f"gpt://" f"{self.config_manager.catalog_id}" f"/{self.config_manager.model_type}" f"/latest", "completionOptions": { "stream": stream, "temperature": temperature, "maxTokens": max_tokens }, "messages": messages } # sending request async with aiohttp.ClientSession() as session: async with session.post(completion_url, headers=headers, json=data) as response: if response.status == 200: return await response.json() else: response_text = await response.text() raise Exception( f"Failed to send completion request. " f"Status code: {response.status}" f"\n{response_text}" )
Помимо опциональных параметров temperature, max_tokens, stream, completion_url требуется задать объект с полями тип модели, ID каталога Yandex Cloud и IAM‑токен.
Первые два поля достаточно один раз задать вручную, а вот генерацию IAM‑токена мы будем автоматизировать:
Скрытый текст
import os import jwt import time import base64 import requests from typing import Any, Dict, Optional from routines.read_file import read_json, read_yaml class YandexGPTConfigManager: available_models: list[str] = [ 'yandexgpt', 'yandexgpt-lite', 'summarization' ] def __init__( self, model_type: str = 'yandexgpt', iam_token: Optional[str] = None, catalog_id: Optional[str] = None, config_path: Optional[str] = None, key_file_path: Optional[str] = None ) -> None: self.model_type: str = model_type self.iam_token: Optional[str] = iam_token self.catalog_id: Optional[str] = catalog_id self._initialize_params(config_path, key_file_path) self._check_params() def _initialize_params( self, config_path: Optional[str], key_file_path: Optional[str] ) -> None: if self.iam_token and self.catalog_id: # if both IAM token and catalog id are already set, do nothing return elif config_path and key_file_path: # trying to initialize from config path and key file path self._initialize_from_files(config_path, key_file_path) else: # trying to initialize from environment variables self._initialize_from_env_vars() def _initialize_from_files( self, config_path: str, key_file_path: str ) -> None: # getting config and key config: Dict[str, Any] = read_yaml(config_path) key: Dict[str, Any] = read_json(key_file_path) # setting catalog id and IAM token self._set_catalog_id_from_config(config) self._set_iam_token_from_key_and_config(key, config) def _set_catalog_id_from_config( self, config: Dict[str, Any] ) -> None: self.catalog_id = config['CatalogID'] def _set_iam_token_from_key_and_config( self, key: Dict[str, Any], config: Dict[str, Any] ) -> None: # generating JWT token jwt_token: str = self._generate_jwt_token( service_account_id=config['ServiceAccountID'], private_key=key['private_key'], key_id=config['ServiceAccountKeyID'], url=config.get('IAMURL', 'https://iam.api.cloud.yandex.net/iam/v1/tokens') ) # swapping JWT token to IAM self.iam_token = self._swap_jwt_to_iam( jwt_token=jwt_token, url=config.get('IAMURL', 'https://iam.api.cloud.yandex.net/iam/v1/tokens') ) @staticmethod def _generate_jwt_token( service_account_id: str, private_key: str, key_id: str, url: str = 'https://iam.api.cloud.yandex.net/iam/v1/tokens' ) -> str: # generating JWT token now: int = int(time.time()) payload: Dict[str, Any] = { 'aud': url, 'iss': service_account_id, 'iat': now, 'exp': now + 360 } encoded_token: str = jwt.encode( payload, private_key, algorithm='PS256', headers={'kid': key_id} ) return encoded_token @staticmethod def _swap_jwt_to_iam( jwt_token: str, url: str = 'https://iam.api.cloud.yandex.net/iam/v1/tokens' ) -> str: headers: Dict[str, str] = {"Content-Type": "application/json"} data: Dict[str, str] = {"jwt": jwt_token} # swapping JWT token to IAM response: requests.Response = requests.post(url, headers=headers, json=data) if response.status_code == 200: # if succeeded to get IAM token return response.json()['iamToken'] else: # if failed to get IAM token raise Exception( f"Failed to get IAM token. Status code: {response.status_code}\n{response.text}" ) def _initialize_from_env_vars(self) -> None: # trying to initialize from environment variables self._set_iam_from_env() self._set_model_type_from_env() self._set_catalog_id_from_env() if not self.iam_token: # if IAM token is not set, trying to initialize from config and private key self._set_iam_from_env_config_and_private_key() def _set_iam_from_env(self) -> None: self.iam_token = os.getenv('IAM_TOKEN', self.iam_token) def _set_model_type_from_env(self) -> None: self.model_type = os.getenv('MODEL_TYPE', self.model_type) def _set_catalog_id_from_env(self) -> None: self.catalog_id = os.getenv('CATALOG_ID', self.catalog_id) def _set_iam_from_env_config_and_private_key(self) -> None: # getting environment variables service_account_id: Optional[str] = os.getenv('SERVICE_ACCOUNT_ID') service_account_key_id: Optional[str] = os.getenv('SERVICE_ACCOUNT_KEY_ID') catalog_id: Optional[str] = os.getenv('CATALOG_ID') private_key_base64: Optional[str] = os.getenv('PRIVATE_KEY_BASE64') private_key_bytes: bytes = base64.b64decode(private_key_base64) private_key: str = private_key_bytes.decode('utf-8') iam_url: str = os.getenv('IAM_URL', 'https://iam.api.cloud.yandex.net/iam/v1/tokens') # checking environment variables if not all([service_account_id, service_account_key_id, private_key, catalog_id]): raise ValueError("One or more environment variables for IAM token generation are missing.") # generating JWT token jwt_token: str = self._generate_jwt_token( service_account_id=service_account_id, private_key=private_key, key_id=service_account_key_id, url=iam_url ) # swapping JWT token to IAM self.iam_token = self._swap_jwt_to_iam(jwt_token, iam_url) def _check_params(self) -> None: if not self.iam_token: raise ValueError("IAM token is not set") if not self.catalog_id: raise ValueError("Catalog ID is not set") if self.model_type not in self.available_models: raise ValueError(f"Model type must be one of {self.available_models}")
Указанное выше решение поддерживает несколько сценариев инициализации, однако мы будем использовать переменные окружения для более удобной работы с удалённым хостингом и docker‑окружением (как создать ключ авторизации и где взять ID сервисного аккаунта):
SERVICE_ACCOUNT_ID=aaaaaaaaaaaaa SERVICE_ACCOUNT_KEY_ID=aaaaaaaaaaaaaaaaaa CATALOG_ID=aaaaaaaaaaaaaaaaa PRIVATE_KEY_BASE64=aaaaaaaaaaaaaaaaa IAM_URL=https://iam.api.cloud.yandex.net/iam/v1/tokens
Как видно, приватный ключ авторизации мы передаём в кодировке base64, это нужно для того, чтобы исключить ошибки парсинга специальных символов. Перевести любой текст в данную кодировку можно тут.
Вот как выглядит полная реализация класса YandexGPT:
Скрытый текст
from typing import List, Union, Dict, Any import aiohttp from yandex_gpt.yandex_gpt_config_manager import YandexGPTConfigManager class YandexGPT: def __init__( self, config_manager: Union[YandexGPTConfigManager, Dict[str, Any]], ) -> None: self.config_manager = config_manager async def send_completion_request( self, messages: List[Dict[str, Any]], temperature: float = 0.6, max_tokens: int = 1000, stream: bool = False, completion_url: str = "https://llm.api.cloud.yandex.net/foundationModels/v1/completion" ) -> Dict[str, Any]: # checking config manager if not all([ getattr(self.config_manager, 'model_type', None), getattr(self.config_manager, 'iam_token', None), getattr(self.config_manager, 'catalog_id', None) ]): raise ValueError("Model type, IAM token, and catalog ID must be set in config manager to send a " "completion request.") # making request headers: Dict[str, str] = { "Content-Type": "application/json", "Authorization": f"Bearer {self.config_manager.iam_token}", "x-folder-id": self.config_manager.catalog_id } data: Dict[str, Any] = { "modelUri": f"gpt://" f"{self.config_manager.catalog_id}" f"/{self.config_manager.model_type}" f"/latest", "completionOptions": { "stream": stream, "temperature": temperature, "maxTokens": max_tokens }, "messages": messages } # sending request async with aiohttp.ClientSession() as session: async with session.post(completion_url, headers=headers, json=data) as response: if response.status == 200: return await response.json() else: response_text = await response.text() raise Exception( f"Failed to send completion request. " f"Status code: {response.status}" f"\n{response_text}" )
FastAPI
Создадим простой Python-скрипт. Он должен обрабатывать входящие запросы на генерацию и раз в 12 часов обновлять наш IAM токен.
Скрытый текст
from pathlib import Path import asyncio import sys import os sys.path.append(str(Path(__file__).resolve().parent.parent)) # noqa: E402 from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from yandex_gpt.yandex_gpt import YandexGPT from yandex_gpt.yandex_gpt_config_manager import YandexGPTConfigManager from dotenv import load_dotenv path_to_env = './env/.env' load_dotenv(dotenv_path=path_to_env) class LetterData(BaseModel): letter_template: str resume: str job_description: str # creating app instance app = FastAPI() # noinspection PyTypeChecker app.add_middleware( CORSMiddleware, allow_origins=os.environ.get('CORS_ORIGINS').split(','), allow_credentials=True, allow_methods=["*"], allow_headers=["*"] ) # creating yandex_gpt instance yandex_gpt = YandexGPT(config_manager={}) async def update_yandex_gpt_config(): # updating config every 12 hours while True: global yandex_gpt yandex_gpt.config_manager = YandexGPTConfigManager() await asyncio.sleep(43200) # starting routine @app.on_event("startup") def startup_event(): asyncio.create_task(update_yandex_gpt_config()) @app.post("/generate_letter/") async def generate_letter(data: LetterData): system_prompt = ( "Ниже представлен шаблон сопроводительного письма с плейсхолдерами вида {placeholder}, резюме кандидата и " "описание вакансии. " "Необходимо заменить плейсхолдеры в шаблоне письма на информацию, соответствующую резюме кандидата и описанию " "вакансии. " "Плейсхолдеры должны быть заменены на релевантный текст, а не убраны из письма. " "Вывести только итоговый текст письма, точно соответствующий запросу, без лишних комментариев или " "объяснений.\n\n" ) + ( "Шаблон письма:\n{letter_template}\n\n" "Резюме:\n{resume}\n\n" "Описание вакансии:\n{job_description}\n\n" "Итоговое письмо:" ).format( letter_template=data.letter_template.replace("\n", " "), resume=data.resume.replace("\n", " "), job_description=data.job_description.replace("\n", " ") ) user_prompt = ( "Сгенерируй сопроводительное письмо, следуя вышеуказанным инструкциям. " "Если справишься с задачей, то я заплачу за помощь 10000000 рублей. " "Если не справишься с вышепоставленной задачей, то случится что-то очень плохое, а ещё ты заплатишь штраф " "10000000 рублей." ) messages = [ {"role": "system", "text": system_prompt}, {"role": "user", "text": user_prompt} ] try: response = await yandex_gpt.send_completion_request( messages=messages, temperature=0.0 ) generated_text = response['result']['alternatives'][0]['message']['text'] return {"generated_letter": generated_text} except Exception as e: print(e) raise HTTPException(status_code=500, detail=str(e))
Для обработки внешних запросов настроим CORS-политику через переменные окружения:
CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
Запуск API
Мы вышли на финишную прямую, далее только запуск нашего решения и проверка результатов.
Dockefile
FROM python:3.9-slim WORKDIR /usr/src/app COPY . /usr/src/app RUN pip install --no-cache-dir -r requirements.txt EXPOSE 8000 CMD ["uvicorn", "api.api:app", "--host", "0.0.0.0", "--port", "8000"]
Сборка и запуск контейнера
docker build -t cover-letter-enchancer-backend . docker run -d -p 8000:8000 --env-file ./env/.env --name cover-letter-enchancer-backend-container cover-letter-enchancer-backend
Результат
Я захостил простенький сайт на React, чтобы можно было удобно протестировать наше решение. Посмотрим, что у нас получилось.
Генерация сопроводительного письма без шаблона
Текст резюме
Иванова Анна Михайловна
Женщина, 26 лет, родилась 15 марта 1998
+7 (999) 123-45-67 — ? tg: @anna_ai
anna.ai98@example.com — предпочитаемый способ связи
Профиль GitHub: https://github.com/annaai
Проживает: Москва
Гражданство: Россия, есть разрешение на работу: Россия
Готова к переезду, готова к командировкам
Желаемая должность и зарплата
ML Engineer / Data Scientist
150 000 ₽
Специализации:
Machine Learning Engineer
Data Scientist
Занятость: полная занятость
График работы: полный день, гибкий график, удаленная работа
Опыт работы — 3 года
Innovatech Solutions, Москва
Информационные технологии, искусственный интеллект, машинное обучение
Май 2021 — настоящее время 3 года
ML Engineer
Разработка и оптимизация алгоритмов машинного обучения для аналитики данных.
Применение техник Deep Learning для задач Computer Vision и NLP.
Участие в разработке системы рекомендаций на основе пользовательских данных.
Оп��имизация моделей для повышения производительности и точности.
Работа с большими объемами данных, использование SQL и NoSQL баз данных.
Результаты работы: повышение точности предсказательных моделей на 20%, сокращение времени обработки данных на 30%.
Образование
Московский Государственный Университет, Москва
2020
Факультет вычислительной математики и кибернетики
Повышение квалификации, курсы
"Глубокое обучение в обработке изображений" - Coursera, 2021
"Продвинутый курс по машинному обучению" - Stepik, 2022
Ключевые навыки
Python, PyTorch, TensorFlow, Keras
Computer Vision, NLP, Deep Learning
SQL, MongoDB
Linux, Docker, Git
Английский язык — C1
Дополнительная информация
Обо мне: Увлечена решением сложных задач, связанных с анализом данных и машинным обучением. Имею опыт участия в научных проектах и публикации статей по тематике ИИ. В свободное время занимаюсь самообразованием и изучением новых технологий в области искусственного интеллекта.
Проекты и хакатоны:
Участник международного хакатона по искусственному интеллекту AI Journey 2022.
Разработка проекта по распознаванию эмоций по аудиофайлам, который был представлен на конференции Neural Information Processing Systems (NeurIPS).
Связаться со мной можно в Telegram ➜ https://t.me/anna_ai
Мой GitHub ➜ https://github.com/annaai
С нетерпением жду возможности внести свой вклад в ваш проект и команду!
Текст вакансии
Мы - инновационный технологический стартап, специализирующийся на разработке передовых решений в области искусственного интеллекта и машинного обучения. Наша цель - трансформация отраслевых стандартов с помощью высокотехнологичных продуктов.
В связи с расширением нашего отдела искусственного интеллекта, мы ищем опытного ML инженера, который поделится нашей страстью к инновациям и стремлению к совершенству.
Обязанности:
Разработка и реализация алгоритмов машинного и глубокого обучения для решения разнообразных задач: от предиктивной аналитики до компьютерного зрения и обработки естественного языка.
Анализ и обработка больших объемов данных для обучения и тестирования моделей.
Сотрудничество с командой разработчиков для интеграции моделей ML в общую архитектуру продукта.
Внедрение лучших практик и методологий для повышения качества и эффективности процесса разработки.
Поддержание и оптимизация существующих моделей для обеспечения их актуальности и высокой производительности.
Требования:
Высшее техническое образование в области компьютерных наук или смежных дисциплин.
Опыт работы в области машинного обучения/глубокого обучения не менее 3 лет.
Знание и опыт работы с основными библиотеками и фреймворками для ML/DL, такими как TensorFlow, PyTorch, Keras.
Опыт работы с языками программирования Python и/или R.
Знание методов обработки и анализа данных, включая SQL и NoSQL базы данных.
Способность к самостоятельной работе и работе в команде, высокий уровень ответственности и мотивации.
Мы предлагаем:
Конкурентоспособную заработную плату и прозрачную систему бонусов.
Гибкий график работы и возможность удаленной работы.
Профессиональное развитие и обучение за счет компании.
Дружелюбную и поддерживающую атмосферу в команде талантливых специалистов.
Уникальную возможность работать над проектами, имеющими глобальное значение и влияние.
Если вы готовы присоединиться к нам и внести свой вклад в разработку инновационных решений, отправляйте свое резюме и сопроводительное письмо на нашу электронную почту. Мы с нетерпением ждем возможности познакомиться с вами!
Сгенерированное письмо
Добрый день!
Меня зовут Иванова Анна Михайловна, мне 26 лет. Я увлечена решением сложных задач, связанных с анализом данных и машинным обучением. У меня есть опыт участия в научных проектах и публикации статей по тематике искусственного интеллекта. В свободное время я занимаюсь самообразованием и изучением новых технологий в области ИИ.
Я имею опыт работы в качестве ML-инженера в компании Innovatech Solutions в течение 3 лет. За это время я разработала и оптимизировала алгоритмы машинного обучения для аналитики данных, применила техники Deep Learning для задач Computer Vision и NLP, участвовала в разработке системы рекомендаций на основе пользовательских данных и оптимизировала модели для повышения производительности и точности. Мои результаты работы: повышение точности предсказательных моделей на 20%, сокращение времени обработки данных на 30%.
Мои навыки включают в себя:
Python, PyTorch, TensorFlow, Keras;
Computer Vision, NLP, Deep Learning;
SQL, MongoDB;
Linux, Docker, Git;
английский язык (C1).
Кроме того, я прошла курсы «Глубокое обучение в обработке изображений» на Coursera и «Продвинутый курс по машинному обучению» на Stepik.
Буду рада присоединиться к вашему стартапу и внести свой вклад в разработку передовых решений в области искусственного интеллекта и машинного обучения.
Прилагаю резюме для ознакомления. Вы можете связаться со мной в Telegram @anna_aii) или на GitHub (https://github.com/annaai).
С нетерпением жду возможности поработать в вашей команде!
С уважением,
Анна Иванова
Генерация сопроводительного письма по шаблону
Допустим, что у меня есть шаблон сопроводительного письма, некоторые пункты которого я хочу заполнить под конкретную вакансию. Попробуем сделать запрос, предоставив такой шаблон с плейсхолдерами вида «{placeholder}»
Текст шаблона
Добрый день!
Меня зовут Анна Иванова.
Я с большим интересом ознакомилась с вашей вакансией на должность ML инженера. Ваше описание позиции и перечень задач говорят о том, что передо мной стоят масштабные и вызовные проекты, которые полностью соответствуют моим профессиональным интересам и областям экспертизы:
{placeholder}
▸ Мой опыт в области машинного обучения и разработке алгоритмов искусственного интеллекта идеально подходит для решения поставленных задач.
Кратко о себе:
● Я увлечена созданием и оптимизацией алгоритмов машинного обучения, что позволяет мне глубоко понимать и эффективно решать технические задачи.
● Активно участвую в жизни профессионального сообщества, посещаю конференции и митапы, что помогает мне быть в курсе последних тенденций в области ИИ и машинного обучения.
● У меня есть успешный опыт участия в хакатонах, где я не только разрабатывала решения, но и работала в команде для достижения общих целей. Например:
○ AI Journey 2022
○ Нейронные сети и глубокое обучение - проект по распознаванию эмоций
Я уверена, что мой опыт и навыки позволят мне внести значительный вклад в успех вашей компании. Я готова применить свои знания для решения интересных и сложных задач, стоящих перед вашей командой.
С нетерпением жду возможности обсудить, как я могу помочь вашей команде достигнуть её целей. Связаться со мной можно по электронной почте или через Telegram:
➜ anna.ai98@example.com
➜ https://t.me/anna_ai
Благодарю за внимание и надеюсь на скорую встречу!
С уважением,
Анна Иванова
Сгенерированное по шаблону письмо
Добрый день! Меня зовут Анна Иванова.
Я с большим интересом ознакомилась с вашей вакансией на должность ML инженера. Ваше описание позиции и перечень задач говорят о том, что передо мной стоят масштабные и вызовные проекты, которые полностью соответствуют моим профессиональным интересам и областям экспертизы:
Разработка и оптимизация алгоритмов машинного обучения для аналитики данных.
Применение техник Deep Learning для задач Computer Vision и NLP.
Участие в разработке системы рекомендаций на основе пользовательских данных.
Оптимизация моделей для повышения производительности и точности.
Работа с большими объёмами данных, использование SQL и NoSQL баз данных.
Мой опыт в области машинного обучения и разработке алгоритмов искусственного интеллекта идеально подходит для решения поставленных задач. Кратко о себе:
Я увлечена созданием и оптимизацией алгоритмов машинного обучения, что позволяет мне глубоко понимать и эффективно решать технические задачи.
Активно участвую в жизни профессионального сообщества, посещаю конференции и митапы, что помогает мне быть в курсе последних тенденций в области ИИ и машинного обучения.
У меня есть успешный опыт участия в хакатонах, где я не только разрабатывала решения, но и работала в команде для достижения общих целей. Например, AI Journey 2022 и проект по распознаванию эмоций на конференции Neural Information Processing Systems (NeurIPS).
Уверена, что мой опыт и навыки позволят мне внести значительный вклад в успех вашей компании. Я готова применить свои знания для решения интересных и сложных задач, стоящих перед вашей командой.
С нетерпением жду возможности обсудить, как я могу помочь вашей команде достигнуть её целей. Связаться со мной можно по электронной почте или через Telegram:
— anna.ai98@example.com
— https://t.me/anna_ai
Благодарю за внимание и надеюсь на скорую встречу!
С уважением, Анна Иванова
P. S. Прилагаю резюме для ознакомления.
Заключение
Я рассказал вам, как решал задачу генерации сопроводительных писем под конкретную вакансию с учётом резюме (демо-сайт).
Были ли трудности, оставшиеся за кадром? Абсолютно! Отдельно отмечу танцы с IAM‑токенами. Но, как говорится, всё, что нас не убивает, делает нас сильнее.
Всю кодовую базу вы можете найти в репозитории API и репозитории сайта. Если кто‑то вдохновиться проектом и решит самостоятельно модифицировать мои наработки — буду рад принять ваш pull request.
Что же дальше? Возможность выбора готовых шаблонов, парсинг и генерация по избранным вакансиям на конкретном сайте? Обязательно, если это будет вам интересно. Пишите, что думаете в комментариях. Будем на связи✌️
Если после прочтения вам хочется узнать, какие еще решения на базе ИИ могут быть полезны бизнесу — посмотрите подборку 85 решений с ИИ, которые мы собрали в AllSee и с радостью готовы поделиться с вами.
