Салют! Меня зовут Григорий, я главный по спецпроектам в AllSee. Если вы когда‑нибудь серьёзно подходили к вопросу поиска работы, то вам определённо приходилось муторно писать сопроводительные письма под каждую вакансию. В данной статье я расскажу, как можно автоматизировать составление релевантного для вакансии сопроводительного письма с учётом вашего резюме.
Какие вводные?
В процессе поиска работы приходится писать индивидуальные сопроводительные письма для каждой вакансии по отдельности. Я хочу автоматизировать данный процесс, генерируя сопроводительное письмо с учётом текста моего резюме и конкретной вакансии, а также иметь возможность сгенерировать его на основе шаблона. Для генерации я буду использовать YandexGPT API, а для обработки входящих запросов — FastAPI.
Routines (служебные функции)
Hidden text
Чтение 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‑токена мы будем автоматизировать:
Hidden text
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:
Hidden text
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 токен.
Hidden text
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.
Что же дальше? Возможность выбора готовых шаблонов, парсинг и генерация по избранным вакансиям на конкретном сайте? Обязательно, если это будет вам интересно. Пишите, что думаете в комментариях. Будем на связи✌️