2.2 👩‍🎨 Дизайн промптов в LangChain

💼 ChatGPT - ваш ручной карьерный консультант

Вы решили найти новую работу и подписались на множество различных тематических чатов и рассылок. Теперь у вас каждый день собирается такое огромное количество вакансий, что обрабатывать их вручную займет много времени.

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

Информация, которая вам необходима, поместится в 5 колонок:

1️⃣ Позиция (job_title).

  • Как указано в описании вакансии, на том же языке. Проверяем полное совпадение, без учета регистра.
  • Если написан грейд, его нужно убрать (например, Senior Python developer -> Python developer, C++ разработчик (middle, senior) ->C++ разработчик).

2️⃣ Компания (company).

  • Как указано в описании вакансии, на том же языке. Проверяем полное совпадение, без учета регистра.
  • Указываем только название (не пишем финтех, крупная компания, мобильная игра и т.д.).

3️⃣ Зарплата (salary).

  • Числа пишем без пробелов, не пишем тыс. или к.(умножаем в таком случае на 1000), не пишем фикс, плюшки, премии, % от продаж, техника и т.д.
  • Не пишем net, gross, на руки и т.д.
  • После числа (диапазона чисел) указываем валюту руб. или $ после пробела.
  • Если указан диапазон, то пишем его через тире, около тире пробелы не ставим (например, 100к-150к рублей -> 100000-150000 руб.).
  • Если указана только нижняя граница (от 2000$ или 100000+руб), то пишем это значение, используя слово от (например, 100к+руб -> от 100000 руб.).
  • Если указана только верхняя граница, то пишем это значение, используя слово до (например, до 100к руб -> до 100000 руб.).
  • Если зарплата указана за час, то в конце добавляем в час.

4️⃣ Контакты для связи  (tg).

  • Указываем контакт в телеграмм, используя @. Проверяем полное совпадение, с учетом регистра.
  • Если указано несколько контаков, то указываем их через запятую (не забывая про пробел после запятой).

5️⃣ Грейд (grade).

  • Возможные значения intern, junior, junior+, middle, middle+, senior, lead.
  • Если указано несколько значений, то пишем их через запятую в порядке возрастания.

Если информация по какой-то из колонок явно не указана в описании вакансии или LLM не может ее найти, поставьте значение None.

Что на входе? Вам предоставляется датасет с двумя столбцами: text_id (уникальный идентификатор вакансии) и text (описание вакансии).

import pandas as pd
df = pd.read_csv('https://stepik.org/media/attachments/lesson/1110806/vacancies_messages_50.csv')

Что на выходе? csv файл, содержащий семь столбцов - text_id, text, job_title, company, salary, tg, grade.

Что использовать? В этой задаче не подразумевается использование сторонних ML библиотек, кроме API ChatGPT и библиотеки pandas для обработки данных.


Замечание: Ваше решение будет зачтено, если в нём будет минимум 120 правильных определенных значений (всего их 250, т.к. в датасете 50 строк и 5 колонок для проверки).


Добрый день!
А я правильно поняла, что мы в template пишем тоже самое, что и в ResponseSchema? Что надо писать туда и туда?

@Мархай_Ольга_Сергеевна, не совсем. Template это шаблон промпта. А РеспонсСхемы мы передаём в аутпут парсер, чтобы ответ модели отформатировался в нужном нам формате. АутпутПарсер формирует нам FormatInstructions, которые мы передаём вместе с темплэйтом в промпт. Итоговый промпт, который подается в модель можете помотреть в нашем шаблоне так:

prompt.format_messages(
        text=df.text[0],
        format_instructions=format_instructions
    )

Ещё можете ниже посмотреть подробный комментарий от Валентина Морозова.

@Иван_Александров, такой же вопрос, я не понимаю, нужно ли писать одно и тоже в template и response_scheme? Или описание нужно писать где-то в одном месте?

template = """
Из следующего текста извлеки информацию:
job_title: Как указано в описании вакансии, на том же языке. Проверяй полное совпадение, без учета регистра. 
            Если написан грейд, его нужно убрать (например, Senior Python developer -> Python developer, C++ разработчик (middle, senior) ->C++ разработчик)
            Если информация явно не определена, поставь значение None
company: Как указано в описании вакансии, на том же языке. Проверяй полное совпадение, без учета регистра. 
        Указывай только название (не пиши финтех, крупная компания, мобильная игра и т.д.)
        Если информация явно не определена, поставь значение None.
salary: Числа пиши без пробелов, не пиши тыс. или к.(умножай в таком случае на 1000), не пиши фикс, плюшки, премии, % от продаж, техника и т.д.
        Не пиши net, gross, на руки и т.д.
        После числа (диапазона чисел) указывай валюту руб. или $ после пробела.
        Если указан диапазон, то пиши его через тире, около тире пробелы не ставь (например, 100к-150к рублей -> 100000-150000 руб.).
        Если указана только нижняя граница (от 2000$ или 100000+руб), то пиши это значение, используя слово от (например, 100к+руб -> от 100000 руб.).
        Если указана только верхняя граница, то пиши это значение, используя слово до (например, до 100к руб -> до 100000 руб.).
        Если зарплата указана за час, то в конце добавляй в час.
        Если информация явно не определена, поставь значение None.
tg: Указывай контакт в телеграмм, используя @. Проверяй полное совпадение, с учетом регистра.
    Если указано несколько контаков, то указывай их через запятую (не забывая про пробел после запятой).
    Если информация явно не определена, поставь значение None.
grade: Возможные значения intern, junior, junior+, middle, middle+, senior, lead.
        Если указано несколько значений, то пиши их через запятую в порядке возрастания.
        Если информация явно не определена, поставь значение None.
text: {text}
{format_instructions}
"""
job_title_schema = ResponseSchema(
    name="job_title",
    description="Как указано в описании вакансии, на том же языке. Проверяй полное совпадение, без учета регистра. "
                "Если написан грейд, его нужно убрать (например, Senior Python developer -> Python developer, C++ разработчик (middle, senior) ->C++ разработчик)"
                "Если информация явно не определена, поставь значение None."
)
company_schema = ResponseSchema(
    name="company",
    description="Как указано в описании вакансии, на том же языке. Проверяй полное совпадение, без учета регистра. "
                "Указывай только название (не пиши финтех, крупная компания, мобильная игра и т.д.)"
                "Если информация явно не определена, поставь значение None."
)
salary_schema = ResponseSchema(
    name="salary",
    description="Числа пиши без пробелов, не пиши тыс. или к.(умножай в таком случае на 1000), не пиши фикс, плюшки, премии, % от продаж, техника и т.д."
                "Не пиши net, gross, на руки и т.д."
                "После числа (диапазона чисел) указывай валюту руб. или $ после пробела."
                "Если указан диапазон, то пиши его через тире, около тире пробелы не ставь (например, 100к-150к рублей -> 100000-150000 руб.)."
                "Если указана только нижняя граница (от 2000$ или 100000+руб), то пиши это значение, используя слово от (например, 100к+руб -> от 100000 руб.)."
                "Если указана только верхняя граница, то пиши это значение, используя слово до (например, до 100к руб -> до 100000 руб.)."
                "Если зарплата указана за час, то в конце добавляй в час."
                "Если информация явно не определена, поставь значение None."
)
tg_schema = ResponseSchema(
    name="tg",
    description="Указывай контакт в телеграмм, используя @. Проверяй полное совпадение, с учетом регистра. "
                "Если указано несколько контаков, то указывай их через запятую (не забывая про пробел после запятой)."
                "Если информация явно не определена, поставь значение None."
)
grade_schema = ResponseSchema(
    name="grade",
    description="Возможные значения intern, junior, junior+, middle, middle+, senior, lead. "
                "Если указано несколько значений, то пиши их через запятую в порядке возрастания."
                "Если информация явно не определена, поставь значение None."
)
response_schemas = [
    job_title_schema,
    company_schema,
    salary_schema,
    tg_schema,
    grade_schema
]
В двух местах не надо. Это всё по итогу собирается в один промпт и подаётся в ЛЛМ.

@Иван_Александров, так, а где тогда не надо писать, в template или в response schema? Не понимаю, как должны выглядеть тогда строчки, которые я написал?

В темплейт не надо добавлять. Респонс инструкции в итоге добавляются в конец промпта.

@Иван_Александров, Я понял, спасибо!

А можете подсказать где ошибка,

Пишет:Expected key `job_title ` to be present, but got {'job_title': 'Python developer'}

responses = [ ]

for text in df['text']:

  prompt = prompt_template.format_messages(text = text, format_instructions = format_instructions)

  response = llm.invoke(prompt)

  output_dict = output_parser.parse(response.content)

  responses.append(response)

@Sergey_Gl, В какой строчке ошибка?

@Петр_Михайлов

Вот, где output_dict

Все нашел баг, поставил лишний пробел

"Что использовать? В этой задаче не подразумевается использование сторонних ML библиотек, кроме API ChatGPT и библиотеки pandas для обработки данных." - а почему такое условие? Мы же тут без langchain вряд ли сможем нормально запромптировать это дело?

@Тимур_Махмутов, имеются ввиду ML-библиотеки - с моделями машинного обучения. Т.е. чтобы решали задачу с помощью ЛЛМ, а не обучая какой-нибудь алгоритм на данных задачи. Langchain, конечно, можно использовать, хотя не обязательно.

У меня на этом шаге закончились токены от курса, что делать?

Напиши мне в тг, добавим токенов)

Проанализировал мой результат с ответами по которым происходила проверка.
Появилось 2 вопроса. Буду рад, если кто-то ответит мне.

1. Как правильно сформулировать подсказку для ChatGPT, чтобы None он присылал не как текст "None", а как пустое значение? 

2. Хотелось бы получить пояснения в чём различие описание полей в template и в поле "description"  у ResponseSchema?
Просто так получилось, что я попробовал сначала сделать как в учебном примере, заполнить одинаково и описание в шаблоне и "description"  у ResponseSchema, но результат был плохим. В итоге, когда я оставил инструкции только в шаблоне (template), а "description" просто указал пустыми строками, то результат был очень хорошим (если не учитывать проблему с None из первого вопроса, то даже отличным).

Наткнулся на подобное и в 3.2.9.
Проведя несколько экспериментов, появилась гипотеза, что если в  "description"  у ResponseSchema писать на русском, то периодически (не всегда) приводит к тому, что ответ от LLM приходит с невалидным форматом, поэтому  output_parser не может правильно обработать (нет закрывающих кавычек ```). 

Это не точно, но очень на это похоже.

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

@Галина_Ершова, кажется, что такой функционал не предусмотрен, но я выложил в форум решений файл, по которому можно будет свериться. Спасибо за идею)

Ошибка может быть при запросе к API, при парсинге ответа и конвертации в словарь. В любом случае цикл прервется.

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

n_trial = 3
errors = []
for text_input in df['text']:
    messages = prompt.format_messages(
        text=text_input,
        format_instructions=format_instructions
    )
    for _ in range(n_trial):
        try:
            response = llm(messages) # Ответ модели
            break
        except:
            response = None
    if response:
        try:
            dict_list.append(output_parser.parse(response.content)) # Переводим содержимое ответа модели в словарь и добавляем в список
        except:
            errors.append((len(dict_list), response)) # Добавляем кортеж с индексом и некорректным ответом
            dict_list.append(None) # Добавляем с список None, если не удалось обработать полученный ответ
    else:
        dict_list.append(None) # Добавляем в список None, если не удалось получить ответ от API

Добрый день!

Для таких же, как я выпускников Онлайн Курсов и далеких от Индустрии любителей делюсь впечатлением.

Не стоит бояться "языка хищника" представленного в блокноте: суть задачи сводиться к заполнению описания (description) в этой части кода:

job_title_schema = ResponseSchema(
    name="job_title",
    description="..."
)
company_schema = ResponseSchema(
    name="company",
    description="..."
)
salary_schema = ResponseSchema(
    name="salary",
    description="..."
)
tg_schema = ResponseSchema(
    name="tg",
    description="..."
)
grade_schema = ResponseSchema(
    name="grade",
    description="..."
)

то есть создания промтов, на основании которых Модель заполнит колонки результирующего датафрейма (job_title, company, salary, tg, grade). Сами промты даны в условии задания.

Все остальное делают объекты библиотеки langchain:

ChatPromptTemplate: создает шаблон запроса, который форматирует текст каждой вакансии и указания о том, какую информацию нужно извлечь Модели;

ResponseSchema: для каждого из требуемых параметров (job_title, company, salary, tg, grade) создает схемы ответов. Эти схемы определяют, какие данные нужно извлечь из ответов Модели;

StructuredOutputParser: используется для преобразования ответа Модели в структурированный формат, удобный для анализа и сохранения.

Далее инициализируеться пустой список (dict_list), который будет использоваться для хранения результатов обработки каждой вакансии. Каждый элемент списка будет словарем, содержащим информацию по заданным категориям (опять же job_title, company, salary, tg, grade).

Затем цикл обработки каждой вакансии. 

1. Выполняется итерация по всем описаниям вакансий в датасете df['text']

2. Формируеться запрос для каждой вакансии: prompt.format_messages(). Эта функция берет текст каждой вакансии (text_input) и форматирует его в соответствии с шаблоном запросов (format_instructions), которые были получены ранее из StructuredOutputParser.

3. Получение ответа от модели response = llm(messages). После формирования запроса он отправляется в Модель (llm), которая генерирует ответ.

4. Обработка ответа модели и сохранение результатов output_parser.parse(response.content) включает преобразование ответа модели в структурированный формат (словарь), используя ранее созданный StructuredOutputParser.

dict_list.append(...) - сформированный словарь с информацией о каждой вакансии добавляется в dict_list.

В результате выполнения этого цикла,  получается список словарей (dict_list), где каждый словарь содержит структурированную информацию об одной вакансии из изначального датасета (df).

Эта информация используется для создания итогового датасета (result_df) .

Удачи!

Комментарий закреплён

Здесь файл с ответами, по которому сравнивается точность в системе. Можно посмотреть и проанализировать разницу со своими ответами :)

Отлично! Теперь можешь оставить свой промпт во вкладке «‎Решения»‎ и посмотреть как решили другие Вы побили порог в 150: ваша точность 250 из 250.

@Никита_Тенишев,

  1. в text_id = 41 в ответе нет "руб." после вилки
  2. в text_id = 1 в тексте всё же есть название компании, а в ответе - нет
  3. в text_id = 7 всё же упоминается компания, но в ответе её нет 

@Александр_Яковлев, Спасибо! Первый и третий пункты поправил (в третьем название компании "AWG"), по второму пункту изменений не сделал, в тексте указано название КА (кадровое агенство) и то, что оно ищет человека для строительной компании, а названия этой строительной компании в тексте я не увидел

job_title_schema = ResponseSchema(
    name="job_title",
    description="Извлеки название позиции, убрав из него указание на грейд (например, 'Senior Python developer' превратить в 'Python developer')."
)
company_schema = ResponseSchema(
    name="company",
    description="Найди и укажи только название компании, предлагающей вакансию, без дополнительных описаний типа 'финтех' или 'мобильная игра'."
)
salary_schema = ResponseSchema(
    name="salary",
    description="Укажи числовое значение зарплаты, приведя его к нужному формату (например, '100к-150к рублей' превратить в '100000-150000 руб.')."
)
tg_schema = ResponseSchema(
    name="tg",
    description="Выдели контакт в Telegram, указанный в описании вакансии, используя '@'. Если указано несколько контактов, перечисли их через запятую."
)
grade_schema = ResponseSchema(
    name="grade",
    description="Определи грейд, указанный в описании вакансии (например, 'intern', 'junior', 'senior'). Если указано несколько грейдов, перечисли их через запятую в порядке возрастания."
)

 Удачи!

Отлично! Теперь можешь оставить свой промпт во вкладке «‎Решения»‎ и посмотреть как решили другие Вы побили порог в 150: ваша точность 194 из 250.

@Морозов_Валентин,  привет!
Хороший результат получился. Скажи, что-то тебе дополнительно пришлось прописывать, чтобы прописывался None, если значение не найдено?.
Просто у меня при указании такой инструкции, "None' выводился как текст, а не как пустое значение. Хочу разобраться, как получать желаемый вывод. 

ваша точность 196 из 250.

template = """Начальные данные: Я решил найти новую работу и подписался на множество различных тематических чатов и рассылок. Теперь у меня каждый день собирается такое огромное количество вакансий, что обрабатывать их вручную займет много времени. Мне нужна твоя помощь в извлечении структурированных данных. Твоя задача:

Внизу представлены ключи и правила их форматирования. Из следующего текста извлеки информацию:

job_title: Наименование позиции. Как указано в описании вакансии, на том же языке. Проверь полное совпадение, без учета регистра.

Если написан грейд, его нужно убрать (например, Senior Python developer -> Python developer, C++ разработчик (middle, senior) ->C++ разработчик.

Если информация отсуствует, верни пустое значение.

company: Наименование компании. Как указано в описании вакансии, на том же языке. Проверь полное совпадение, без учета регистра.

Укажи только наименование (не пиши финтех, крупная компания, мобильная игра и т.д.). Если информация отсуствует, верни пустое значение.

salary: Зарплата. Верни число, не пиши тыс. или к.(в таком случае умножь на 1000), не пиши фикс, плюшки, премии, % от продаж, техника и т.д.

Не пиши net, gross, на руки и т.д.

После числа (диапазона чисел) укажи валюту руб. или $ после пробела.

Если указан диапазон, то пиши его через тире, около тире пробелы не ставь (например, 100к-150к рублей -> 100000-150000 руб.).

Если указана только нижняя граница (от 2000$ или 100000+руб), то пиши это значение, используя слово от (например, 100к+руб -> от 100000 руб.).

Если указана только верхняя граница, то пиши это значение, используя слово до (например, до 100к руб -> до 100000 руб.).

Если зарплата указана за час, то в конце добавь в час.

Если информация отсуствует, верни пустое значение.

tg: Контакты для связи. Укажи контакт в телеграмм, используя @. Проверь полное совпадение, с учетом регистра.

Если указано несколько контактов, то укажи их через запятую (не забывая про пробел после запятой).

Если информация отсуствует, верни пустое значение.

grade: Грейд. Возможные значения intern, junior, junior+, middle, middle+, senior, lead.

Если указано несколько значений, то пиши их через запятую в порядке возрастания.

Если информация отсуствует, верни пустое значение.

text: {text}

{format_instructions}

"""

prompt = ChatPromptTemplate.from_template(template=template)

 

job_title_schema = ResponseSchema(

name="job_title",

description="Наименование позиции в вакансии"

)

 

company_schema = ResponseSchema(

name="company",

description="Наименование компании в вакансии"

)

 

salary_schema = ResponseSchema(

name="salary",

description="Зарплата в вакансии"

)

 

 

tg_schema = ResponseSchema(

name="tg",

description="Контакт для связи в телеграмме указанный в вакансии"

)

 

grade_schema = ResponseSchema(

name="grade",

description="Указанный грейд в вакансии"

)

 

response_schemas = [

job_title_schema,

company_schema,

salary_schema,

tg_schema,

grade_schema

]

Отлично! Теперь можешь оставить свой промпт во вкладке «‎Решения»‎ и посмотреть как решили другие Вы побили порог в 120: ваша точность 196 из 250.

template = """

Начальные данные: Я решил найти новую работу и подписался на множество различных тематических чатов и рассылок.

Теперь у меня каждый день собирается такое огромное количество вакансий, что обрабатывать их вручную займет много времени.

Мне нужна твоя помощь в извлечении структурированных данных.

Твоя задача:

Внизу представлены ключи и правила их форматирования. Из следующего текста извлеки информацию:

 

job_title: Позиция. Как указано в описании вакансии, на том же языке. Проверяем полное совпадение, без учета регистра.

Если написан грейд, его нужно убрать (например, Senior Python developer -> Python developer, C++ разработчик (middle, senior) ->C++ разработчик.

Если информация отсуствует, то напиши None.

 

company: Название позиции в вакансии. Как указано в описании вакансии, на том же языке. Проверяем полное совпадение, без учета регистра.

Указываем только название (не пишем финтех, крупная компания, мобильная игра и т.д.). Если информация отсуствует, то напиши None.

 

salary: Зарплата. Числа пишем без пробелов, не пишем тыс. или к.(умножаем в таком случае на 1000), не пишем фикс, плюшки, премии, % от продаж, техника и т.д.

Не пишем net, gross, на руки и т.д.

После числа (диапазона чисел) указываем валюту руб. или $ после пробела.

Если указан диапазон, то пишем его через тире, около тире пробелы не ставим (например, 100к-150к рублей -> 100000-150000 руб.).

Если указана только нижняя граница (от 2000$ или 100000+руб), то пишем это значение, используя слово от (например, 100к+руб -> от 100000 руб.).

Если указана только верхняя граница, то пишем это значение, используя слово до (например, до 100к руб -> до 100000 руб.).

Если зарплата указана за час, то в конце добавляем в час.

Если информация отсуствует, то напиши None.

 

tg: Контакты для связи. Указываем контакт в телеграмм, используя @. Проверяем полное совпадение, с учетом регистра.

Если указано несколько контаков, то указываем их через запятую (не забывая про пробел после запятой).

Если информация отсуствует, то напиши None.

 

grade: Грейд. Возможные значения intern, junior, junior+, middle, middle+, senior, lead.

Если указано несколько значений, то пишем их через запятую в порядке возрастания.

Если информация отсуствует, то напиши None.

 

text: {text}

 

{format_instructions}

"""

 

job_title_schema = ResponseSchema(

name="job_title",

description="Название позиции в вакансии"

)

company_schema = ResponseSchema(

name="company",

description="Название компании в вакансии"

)

salary_schema = ResponseSchema(

name="salary",

description="Зарплата в вакансии"

)


 

tg_schema = ResponseSchema(

name="tg",

description="Контакт для связи в телеграмме указанный в вакансии"

)

grade_schema = ResponseSchema(

name="grade",

description="Указанный грейд в вакансии"

)

response_schemas = [

job_title_schema,

company_schema,

salary_schema,

tg_schema,

grade_schema

]

Отлично! Теперь можешь оставить свой промпт во вкладке «‎Решения»‎ и посмотреть как решили другие Вы побили порог в 120: ваша точность 188 из 250.

Мое решение получило 163 из 250. Вот код: 

from utils import ChatOpenAI

from getpass import getpass

#course_api_key= "Введите ваш API ключ с курса"

course_api_key = getpass(prompt='Введите API ключ')

# Инициализируем языковую модель

llm = ChatOpenAI(course_api_key=course_api_key)

from tqdm import tqdm

import pandas as pd

from langchain.output_parsers import ResponseSchema

from langchain.output_parsers import StructuredOutputParser

from langchain.prompts import ChatPromptTemplate

df = pd.read_csv('https://stepik.org/media/attachments/lesson/1110806/vacancies_messages_50.csv')

# Шаблон запроса к модели для извлечения информации

template = """

Из следующего текста извлеки информацию:

 

Позиция (job_title): Извлеки наименование позиции, как оно указано в описании вакансии, на том же языке, игнорируя регистр. Убери грейд, если он указан (например, "Senior Python Developer" преобразуется в "Python Developer").

 

Компания (company): Извлеки название компании, как оно указано в описании вакансии, на том же языке, игнорируя регистр. Укажи только название компании без дополнительных описаний.

 

Зарплата (salary): Извлеки информацию о зарплате, указывая числа без пробелов. Не используй сокращения. После числа (диапазона чисел) укажи валюту ("руб." или "$") после пробела. Используй форматирование для диапазона без пробелов около тире и добавь "в час", если зарплата указана за час.

 

Контакты для связи (tg): Укажи контакт в Telegram, используя "@". Проверь полное совпадение, с учетом регистра. Если указано несколько контактов, перечисли их через запятую с пробелом после запятой.

 

Грейд (grade): Укажи возможный грейд (intern, junior, junior+, middle, middle+, senior, lead). Если указано несколько грейдов, перечисли их через запятую в порядке возрастания.

 

Текст вакансии:

{text}

 

{format_instructions}

"""

 

# Создание шаблона запроса

prompt = ChatPromptTemplate.from_template(template=template)

 

# Определение схем ответов

response_schemas = [

    ResponseSchema(name="job_title", description="Извлечение наименования позиции без указания грейда."),

    ResponseSchema(name="company", description="Извлечение названия компании без дополнительных описаний."),

    ResponseSchema(name="salary", description="Извлечение зарплаты с указанием валюты, обработка диапазонов и часовых ставок."),

    ResponseSchema(name="tg", description="Извлечение контактов Telegram с точным совпадением."),

    ResponseSchema(name="grade", description="Определение грейда позиции, если он указан.")

]

 

prompt = ChatPromptTemplate.from_template(template=template)

# Инициализация парсера ответов

output_parser = StructuredOutputParser.from_response_schemas(response_schemas)

format_instructions = output_parser.get_format_instructions()

 

# Подготовка списка для собранных данных

dict_list = []

 

# Обработка каждого текста вакансии

for text_input in tqdm(df['text']):

    formatted_message = prompt.format_messages(text=text_input, format_instructions=format_instructions)

    response = llm(formatted_message)

    # Убедимся, что обращаемся к правильному полю объекта ответа и что это поле является строкой

    response_text = response.content if hasattr(response, 'content') else str(response)

    dict_list.append(output_parser.parse(response_text))

# Создание датафрейма из собранных данных

result_df = pd.DataFrame(dict_list)

# Объединение результатов с исходным датасетом

ans_df = pd.concat([df, result_df], axis=1)

# Сохранение итогового датафрейма в CSV-файл

ans_df.to_csv('final_vacancies_info.csv', index=False)

Отлично! Теперь можешь оставить свой промпт во вкладке «‎Решения»‎ и посмотреть как решили другие Вы побили порог в 120: ваша точность 163 из 250.

# Вопрос None vs  'None'  решил подстановкой null => далее без костылей в коде

# Случай перевода вида "100000 - 200000 рублей' к виду '100000-200000 руб.' как то не особо удался. Конверсия примерно срабатывала в # 1 случае из 3-4x

job_title_schema = ResponseSchema(

    name="job_title",

    description="extract job title. stick to text language. ignore position grade such as junior, senior."

)


 

company_schema = ResponseSchema(

    name="company",

    description="extract company name only. If company name does not preset, use null"

)

 

salary_schema = ResponseSchema(

    name="salary",

    description="""

   Извлечение значений зарплаты. Все дальнейшие уточнения относятся только к зарплате.

   Извлеките числовые значения без пробелов и индикатора валюты зарплаты.  

   если информация по зарплате содержит индикатор тысячи (примеры 'k', 'к','k.', 'к.', 'тыс.') ,умножьте каждое извлеченное значение на 1000.

Не включайте термины типа 'net,' 'gross,' 'на руки,' и т.д.

Если указан диапазон, напишите его через дефис: (верный пример: "6000-9000 " , неверныепримеры : [ " 6000 9000 ", неверные примеры: ["6000 - 9000 ", " 6000 6000- 9000"] )

Если указана только нижняя граница зарплаты(например, "от 2000"), напишите это как "от 2000", при необходимости умножить на 1000

Если указана только верхняя граница зарплаты(например, "до 100000 руб"),напишите это как "до 100000", при необходимости умножить на 1000 .

Производить замены: 'рублей' на 'руб.', '₽' на 'руб.', 'долларов' на $

Информация о зарплате обязательно заканчивается пробелом (' ') , за ним следует обозначением валюты: руб. или $.

Если валюту зарплаты не возможно определить, указывать 'руб.'

    """

)

 

tg_schema = ResponseSchema(

    name="tg",

    description="telegram style as @account. if tg value could not be identified, use null "

)


 

grade_schema = ResponseSchema(

    name="grade",

    description="""indicate a position grade. examples [intern, junior, junior+, middle, middle+, senior, lead]

    in text grade may also follow immediately after '#'

    if more than one value is found, show them all ordered ascending by the rank value

    if grade cannot be identified, use null

    """

)

Отлично! Теперь можешь оставить свой промпт во вкладке «‎Решения»‎ и посмотреть как решили другие Вы побили порог в 120: ваша точность 199 из 250.

Один и тот же промпт (практически такой же как и у остальных) на gpt-3 выдавал 160 совпадений, а на gpt-4 уже 197

Отлично! Теперь можешь оставить свой промпт во вкладке «‎Решения»‎ и посмотреть как решили другие Вы побили порог в 120: ваша точность 197 из 250.

Несмотря на подробнейшую инструкцию все равно по самому краю trashold получилось пройти, хоть и с первого раза

Много срезалось на поле позиции, т.к. категорически не хотел вырезать грейд из позиции

prompt_template = """
Из описания вакансии нужно определить следующие сущности, все сущности необходимо вывести в итог строго как написано в
описании вакансии, включая язык, синтаксис, пунктуацию и прочее:
1. Позиция (job_title), если в названии позиции указан грейд (intern, junior, junior+, middle, middle+, senior, lead) - вывести только название позиции, название ГРЕЙД удалить! (название как в объявлении с учетом языка). 
2. Название компании (company), строго имя собственное без указания отрасли / размера и общих фраз (строго как в объявлении)
3. Зарплата (salary). Строго в числовом выражении, если встречается "тыс." или "к", то умножаем значение на 1000. 
Также указываем валюту из объявления - "руб." (для рублей) или "$" (для долларов США) через пробел после суммы.
Если указана конкретная сумма, то указать её (например "100000 руб."), если дана вилка то указать через тире (например, "1000-1500 " ) , еслисуммауказанаснижнейиливерхнейграницей , тоуказатьтак : " до 100000 руб . " или " от 1000 "), если сумма указана с нижней или верхней границей, то указать так: "до 100000 руб." или "от 1000 ".
Если зарплата указана за чат, то в конце добавить "в час". Если зарплата представлена миллионами, то считать это ошибкой и делить на 1000.
4. Контакты для связи telegram / телеграм (tg). Контакт выводим в ответ с учтетом впереди стоящего символа @, с учетом регистра строго как в объявлении.
Если представлено несколько контактов, вывести каждый, через запятую.
5. Грейд (grade) может содержаться как тексте позиции, так и в отдельном поле. 
Доступные грейды в порядке возрастания: [intern, junior, junior+, middle, middle+, senior, lead].
Если указано несколько значений, то пишем их через запятую строго в порядке возрастания (в нижнем регистре).
Прочие инструкции: 
- если сущности нет в объявлении в явном виде, то вместо нее вывести значение None
- вводим в ответ все поля в порядке как указаны выше черзе точку с запятой (в т.ч. None для неопределившихся) в виде: jub_title;company;salary;tg;grade

Ниже представлены 3 примера с ответами.
====================================================================================================
Пример 1 
#вакансия #удалено #C #NET 
🔥C# Engineer🔥
- Company: GGEM
- Format: Remote
- Employment: Полная
- Salary: Почасовая 17-20$
- Выплата как в Валюте так и в Крипте
🚀GGEM is the ultimate gaming ecosystem bridging Web2 and Web3 gaming communities. The platform brings together a gaming portal, in-game asset renting marketplace, academy for gamers, big gaming events, tournaments, and more.
Contact: @AlexMayel

Ответ:
C# Engineer;GGEM;17-20 $ в час;@AlexMayel;None

Пример 2
#вакансия #node #vue #fullstack #удаленно 
Формат работы: Удаленно, на момент оформление нужно быть в РФ
Занятость: Полная 
Компания: КА "АйтиКА" ищет для заказчика 
Зарплатная вилка: 300-380 т.р. на руки
Варианты трудоустройства: ТК РФ
Ищу Full-stack разработчика (Node.js, Vue.js, Nuxt.js) в строительную компанию, которая более 20 лет занимается постройкой элитной жилой и коммерческой недвижимости. Компания имеет it-аккредитацию. Проект - корпоративные сайты. Задачи от простых (поправить, заменить) до реализовать выборщик или калькулятор. В команде 5 человек. Всего 1 этап интервью.
Что нужно делать:
- Разрабатывать новые модули на сайтах, проводить их оптимизацию.
- Принимать участие в разработке ТЗ на совершенствование сайтов.
- Разрабатывать техническую документацию на созданные программные решения.
- Сопровождать внедрение новых решений, связанных с сайтами.
Интересно? Давай обсудим детали @artur_vv

Ответ:
Full-stack разработчик;КА "АйтиКА";300000-380000 руб.;@artur_vv;None

Пример 3
#вакансия #удаленно #системный_аналитик
Компания: Amber
Позиция: Системный аналитик
Грейд: Middle/Senior
ЗП: до 300 000 тыс руб
Локация: СТРОГО РФ
Всем привет! 
МЫ сейчас в поиске системного аналитика на банковский проект
Описание проекта:
Провести  анализ по выявлению систем источников для построения CVM отчетности 1 приоритета
Чем предстоит заниматься:
-Заработная плата обсуждается индивидуально с успешным кандидатом.
Связаться со мной,
@Sholpan771.

Ответ:
Системный аналитик;Amber;до 300000 руб.;@Sholpan771;middle,senior
====================================================================================================

Context: {text_input}

Question: Выдели (распознай и выведи в результат) требуемые сущности: 1. позиция (без грейда), 2. компания, 3. зарплата, 4. контакты, 5. грейд  

Answer: jub_title;company;salary;tg;grade"""

Отлично! Теперь можешь оставить свой промпт во вкладке «‎Решения»‎ и посмотреть как решили другие Вы побили порог в 150: ваша точность 153 из 250.

Основную проблему составила обработка отсутствующих данных: если просить в таких случаях ответ в виде пустой строки или None, то парсер иногда выкидывает ошибку. Решение проблемы простое: попросить отвечать какой-то заранее заготовленной строкой и после её обрабатывать. 

job_title_schema = ResponseSchema(
    name="job_title",
    description="""На какую позицию/вакансию ищут кандидата? 
Не указывай уровень, просто напиши название позиции, 
например, для названия 'Senior Python developer' - это просто 'Python developer', а 'C++ разработчик (middle, senior)' 'C++ разработчик'
Из названия удали grade, то есть уровень. Название позиции не должно содержать слов junior, middle, senior, lead.
Если эта информация напрямую не указана в вакансии, ответь 'не знаю'.
""",
)
company_schema = ResponseSchema(
    name="company",
    description="""Напиши название компании, для которой ищут кандидата. 
Название напиши на том же языке, что и в тексте, точно так же, как указано в вакансии.
В качестве ответа требуется только название, без дополнительных пояснений (например, что это крупная компания, финтех или прочее)
Если эта информация напрямую не указана в вакансии, ответь 'не знаю'.""",
)
salary_schema = ResponseSchema(
    name="salary",
    description="""Укажи предполагаемую в вакансии зарплату в соответствии со следующими правилами:
1) Не пиши 'тыс.' или 'к.', если такие конструкции встречаются в тексте, умножай на 1000 указанное до них число.
2) Не пиши 'net', 'gross', 'на руки' и подобные им.
3) После числа (или диапазона чисел, если указан диапазон возможных зарплат), укажи валюту, в которой будут платить. Если это рубли, то напиши после числа (чисел) 'руб.', а если доллары - '$'  ДО ДОЛЛАРОВ НЕ ЗАБУДЬ ПОСТАВИТЬ ПРОБЕЛ.
4) Если указан диапазон, то напиши его через тире, около тире пробелы не ставь (например, '100к-150к рублей' -> '100000-150000 руб.').
5) Если указана только нижняя граница (от 2000$ или 100000+руб), то напиши это значение, используя слово 'от' (например, '100к+руб' -> 'от 100000 руб.').
6) Если указана только верхняя граница, то напиши это значение, используя слово 'до' (например, 'до 100к руб' -> 'до 100000 руб.').
7) Если зарплата указана за час, то в конце добавь 'в час'.
Если эта информация напрямую не указана в вакансии, ответь 'не знаю'.
""",
)
tg_schema = ResponseSchema(
    name="tg",
    description="""Укажи контакт для связи. Контакт перепиши в точности как в тексте, с учетом регистра, при необходимости добавив '@' перед ним (такой символ для каждого контакта должен быть один)
Если указано несколько контаков, то укажи их через запятую (не забывая про пробел после запятой).
Если эта информация напрямую не указана в вакансии, ответь 'не знаю'.""",
)
grade_schema = ResponseSchema(
    name="grade",
    description="""Напиши уровень, на который ищут кандидата. Возможные значения: 'intern', 'junior', 'junior+', 'middle', 'middle+', 'senior', 'lead'.
Если указано несколько значений, то напиши их через запятую в порядке возрастания.
Если эта информация напрямую не указана в вакансии, ответь 'не знаю'.
""",
)
response_schemas = [
    job_title_schema,
    company_schema,
    salary_schema,
    tg_schema,
    grade_schema,
]

Далее по аналогии с ноутбуком из курса, в конце обработка значений "не знаю"
 

job_titles = []
companies = []
salaries = []
tgs = []
grades = []
for job_text in df["text"]:
    messages = prompt.format_messages(
        job_text=job_text,
        format_instructions=format_instructions,
    )
    response = chat(messages)
    output_dict = output_parser.parse(response.content)
    job_titles.append(output_dict.get("job_title") if output_dict.get("job_title") != "не знаю" else "")
    companies.append(output_dict.get("company") if output_dict.get("company") != "не знаю" else "")
    salaries.append(output_dict.get("salary") if output_dict.get("salary") != "не знаю" else "")
    tgs.append(output_dict.get("tg") if output_dict.get("tg") != "не знаю" else "")
    grades.append(output_dict.get("grade") if output_dict.get("grade") != "не знаю" else "")

Отлично! Теперь можешь оставить свой промпт во вкладке «‎Решения»‎ и посмотреть как решили другие Вы побили порог в 150: ваша точность 206 из 250.

template = """Из следующего текста извлеки информацию:

 

job_title: Определи позицию, должность. Например,  Backend Engineer, Python developer, C++ разработчик.   Если написан грейд, то есть (intern,  lead,   Senior, middle,junior) его нужно убрать.Если не найдешь, напиши "None"

 

company: Укажи название компании. Не указывай не пишем финтех, крупная компания, мобильная игра и пр. Если не найдешь, напиши "None"

 

salary:Зарплата. Числа пишем без пробелов, не пишем тыс. или к.(умножаем в таком случае на 1000), не пишем фикс, плюшки, премии, % от продаж, техника и т.д.

Не пишем net, gross, на руки и т.д. После числа (диапазона чисел) указываем валюту руб. или $ после пробела.

Если указан диапазон, то пишем его через тире, около тире пробелы не ставим (например, 100к-150к рублей надо написать 100000-150000 руб.).

Если указана только нижняя граница (от 2000$ или 100000+руб), то пишем это значение, используя слово от (например, 100к+руб надо написать от 100000 руб.).

Если указана только верхняя граница, то пишем это значение, используя слово до (например, до 100к руб -> до 100000 руб.).Если не найдешь, напиши None

Если зарплата указана за час, то в конце добавляем в час.

 

tg: Указываем контакт в телеграмм, используя @. Проверяем полное совпадение, с учетом регистра.

Если указано несколько контаков, то указываем их через запятую (не забывая про пробел после запятой).Если не найдешь, напиши None

 

grade: Возможные значения intern, junior, junior+, middle, middle+, senior, lead.

Если указано несколько значений, то пишем их через запятую в порядке возрастания.Если не найдешь, напиши None

 

text: {text}

 

{format_instructions}

"""

Отлично! Теперь можешь оставить свой промпт во вкладке «‎Решения»‎ и посмотреть как решили другие Вы побили порог в 150: ваша точность 162 из 250.

Спасибо за готовое описание)

jt_schema = ResponseSchema(name="job_title",
                             description="Как указано в описании вакансии, на том же языке. Проверяем полное совпадение, без учета регистра. Если написан грейд, его нужно убрать ОБЯЗАТЕЛЬНО (например, Senior Python developer -> Python developer, C++ разработчик (middle, senior) ->C++ разработчик).")

comp_schema = ResponseSchema(name="company",
                                      description="Как указано в описании вакансии, на том же языке. Проверяем полное совпадение, без учета регистра.Указываем только название (не пишем финтех, крупная компания, мобильная игра и т.д.).")

sal_schema = ResponseSchema(name="salary",
                                    description="Числа пишем без пробелов, не пишем тыс. или к.(умножаем в таком случае на 1000), не пишем фикс, плюшки, премии, % от продаж, техника и т.д. Не пишем net, gross, на руки и т.д. После числа (диапазона чисел) указываем валюту руб. или $ после пробела.Если указан диапазон, то пишем его через тире, около тире пробелы не ставим (например, 100к-150к рублей -> 100000-150000 руб.). Если указана только нижняя граница (от 2000$ или 100000+руб), то пишем это значение, используя слово от (например, 100к+руб -> от 100000 руб.). Если указана только верхняя граница, то пишем это значение, используя слово до (например, до 100к руб -> до 100000 руб.). Если зарплата указана за час, то в конце добавляем в час.")

tg_schema = ResponseSchema(name="tg",
                                    description="Указываем контакт в телеграмм, используя @. Проверяем полное совпадение, с учетом регистра. Если указано несколько контаков, то указываем их через запятую (не забывая про пробел после запятой).")

grade_schema = ResponseSchema(name="grade",
                                    description="Возможные значения intern, junior, junior+, middle, middle+, senior, lead. Если указано несколько значений, то пишем их через запятую в порядке возрастания.")

Вы побили порог в 150: ваша точность 187 из 250.

path = 'https://stepik.org/media/attachments/lesson/1110806/vacancies_messages_50.csv'
df = pd.read_csv(path)
job_title_schema = ResponseSchema(name="job_title",
                             description="Позиция. Прям как в тексте, но убери слова: intern, junior, junior+, middle, middle+, senior, lead")
company_schema = ResponseSchema(name="company",
                                description="Компания. Не выводи в названии пояснения: финтех, крупная компания, мобильная игра и т.д.")
salary_schema = ResponseSchema(name="salary",
                               description="Зарплата. Убрать слова: net, gross, на руки. Указания {тыс.} и {к} НЕ пишем, а число умножаем на 1000. Формат по шаблону СТРОГО: {руб.} или {$}. Если есть диапазон, то СТРОГИЙ формат '{число}-{число} {валюта}'. Если указана одна граница, то СТРОГИЙ формат: '{от до} {число} {валюта}'. Дополнительные слова после валюты НЕ писать. Если зарплата указана в час, то ДОБАВИТЬ в конце 'в час'.")
tg_schema = ResponseSchema(name="tg",
                           description="Контакты для связи. Формат: ${} или '${}, ${}, ... ,${}'.")
grade_schema = ResponseSchema(name="grade",
                            description="Грейд. Формат только: 'intern, junior, junior+, middle, middle+, senior, lead' порядок важен.")
response_schemas = [job_title_schema, 
                    company_schema,
                    salary_schema,
                    tg_schema,
                    grade_schema]
output_parser = StructuredOutputParser.from_response_schemas(response_schemas)
format_instructions = output_parser.get_format_instructions()
hr_template = """
Из следующего текста извлеки информацию:
job_title: Позиция.
Прям как в тексте, но убери слова: intern, junior, junior+, middle, middle+, senior, lead
company: Компания.
Не выводи в названии пояснения: финтех, крупная компания, мобильная игра и т.д.
salary: Зарплата.
Убрать слова: net, gross, на руки. Указания 'тыс.' и 'к' НЕ пишем, а число умножаем на 1000. Формат по шаблону СТРОГО: 'руб.' или '$'. Если есть диапазон, то СТРОГИЙ формат "'число'-'число' 'валюта'". Если указана одна граница, то СТРОГИЙ формат: "'от до' 'число' 'валюта'". Дополнительные слова после валюты НЕ писать. Если зарплата указана в час, то ДОБАВИТЬ в конце 'в час'.
tg: Контакты для связи.
Формат: $'' или "$'', $'', ... , $''".
grade: Грейд.
Формат только: 'intern, junior, junior+, middle, middle+, senior, lead' порядок важен.
text: {text}
{format_instructions}
"""
dict_list = []
for text_input in tqdm(df['text']):
    messages = prompt.format_messages(text=text_input,
                                  format_instructions=format_instructions)
    response = openai_llm(messages)
    dict_list.append(output_parser.parse(response.content))
df_res = pd.DataFrame(dict_list)
df_sub = pd.concat([df, df_res], axis=1)
df_sub.to_csv('submission_2.2.8.csv', index=False)

Решение не идеальное (можно переформулировать для Salary и Company, остальные, вроде, неплохо парсит), но количество токенов уже поджимает))

P.S. Задачи очень интересные

Вы побили порог в 150: ваша точность 167 из 250.

Попробовал сделать что-то такое:

def make_prompt(txt):

prompt = """

Веди себя как HR-программа.

Твоя задача - классифицировать заданный текст на такие логические части:

**Позиция работы:

Как указано в описании вакансии, на том же языке. Проверяем полное совпадение, без учета регистра.

***Если написано слово, уточняющее опыт его нужно убрать***

Например:\n

*Senior Python developer -> Python developer

\n

**Компания:

* Как указано в описании вакансии, на том же языке. Проверяем полное совпадение, без учета регистра.

* Указываем только название (не пишем финтех, крупная компания, мобильная игра и т.д.).

\n

***Зарплата:

*Не пишем net, gross, на руки и т.д.

*После числа (диапазона чисел) указываем валюту руб. или $ после пробела.

*Если указан диапазон, то пишем его через тире, около тире пробелы не ставим

*Если указана только нижняя граница ( Например от 2000$ или 100000+руб), то пишем это значение, используя слово от.

Например:\n

* 100000руб -> от 100000 руб.

\n

**Грейд:

* Возможные значения intern, junior, junior+, middle, middle+, senior, lead.

* Если указано несколько значений, то пишем их через запятую в порядке возрастания.

Например:\n

* middle,junior -> junior,middle \n

***Форма ответа:\n

Ответ:\n

Позиция работы (job title): [введите текст здесь]

Компания (company): [введите текст здесь]

Зарплата (salary): [введите текст здесь]

Грейд (grade): [введите текст здесь]

***

\nТекст для анализа:\n ***

"""

 

prompt+= f'{txt}*** \nОтвет:\n'

return prompt


Возникли проблемы только с ЗП, не стал 2 итерацию запросов делать. Тг с помощью регулярок полностью парсится. 
Не убивайте муху из пушки: решайте задачки более простыми способами, где это возможно, пощадите озоновый слой, свои токены и так далее и так далее и так далее

Вы побили порог в 150: ваша точность 182 из 250.