Pull to refresh

СoverLetterEnchancer: упрощаем поиск работы с FastAPI и YandexGPT

Level of difficultyEasy
Reading time15 min
Views3.2K

Салют! Меня зовут Григорий, я главный по спецпроектам в 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 баз данных.

Мой опыт в области машинного обучения и разработке алгоритмов искусственного интеллекта идеально подходит для решения поставленных задач. Кратко о себе:

  1. Я увлечена созданием и оптимизацией алгоритмов машинного обучения, что позволяет мне глубоко понимать и эффективно решать технические задачи.

  2. Активно участвую в жизни профессионального сообщества, посещаю конференции и митапы, что помогает мне быть в курсе последних тенденций в области ИИ и машинного обучения.

  3. У меня есть успешный опыт участия в хакатонах, где я не только разрабатывала решения, но и работала в команде для достижения общих целей. Например, AI Journey 2022 и проект по распознаванию эмоций на конференции Neural Information Processing Systems (NeurIPS).

Уверена, что мой опыт и навыки позволят мне внести значительный вклад в успех вашей компании. Я готова применить свои знания для решения интересных и сложных задач, стоящих перед вашей командой.

С нетерпением жду возможности обсудить, как я могу помочь вашей команде достигнуть её целей. Связаться со мной можно по электронной почте или через Telegram:

anna.ai98@example.com
https://t.me/anna_ai

Благодарю за внимание и надеюсь на скорую встречу!

С уважением, Анна Иванова

P. S. Прилагаю резюме для ознакомления.

Заключение

Я рассказал вам, как решал задачу генерации сопроводительных писем под конкретную вакансию с учётом резюме (демо-сайт).

Были ли трудности, оставшиеся за кадром? Абсолютно! Отдельно отмечу танцы с IAM‑токенами. Но, как говорится, всё, что нас не убивает, делает нас сильнее.

Всю кодовую базу вы можете найти в репозитории API и репозитории сайта. Если кто‑то вдохновиться проектом и решит самостоятельно модифицировать мои наработки — буду рад принять ваш pull request.

Что же дальше? Возможность выбора готовых шаблонов, парсинг и генерация по избранным вакансиям на конкретном сайте? Обязательно, если это будет вам интересно. Пишите, что думаете в комментариях. Будем на связи✌️

Tags:
Hubs:
Total votes 6: ↑3 and ↓30
Comments2

Articles