Transcription
Всем привет, друзья! Это моё первое видео после долгого застоя. Не снимал я видео по причинам, которые вам не интересны. Так что давайте сразу перейдём к теме ролика: о том, как создать своего AI-агента на Python в 2025 году. Было бы лучше сначала объяснить вам вообще, что такое AI-агенты и чем они отличаются от обычных чат-ботов с нейронкой.
В целом, в этой картинке расписаны различия между обычной нейронкой, типа GPT, и AI-агентом. Ну, самое основное то, что AI-агенты принимают решение сами, без помощи пользователя. То есть, они более автономные, и у них чаще больше функционала, нежели у обычных чат-нейронок, типа GPT. И также они умеют сами себя исправлять и проверять самих себя на наличие ошибок. Но в целом, мне кажется, вам будет понятнее дальше в ходе видеоурока, поэтому сильно углубляться в это мы не будем.
Но, но, но, но, но, но, прежде чем мы начнём, я ожидаю, что ты, именно ты, дружище, знаешь основы Python и программирования. Типа, понимаешь, как работают API, как устанавливать библиотеки Python и использовать их. Круто. Давайте теперь начинать.
Так, давайте кратко расскажу, что мы будем вообще разрабатывать. У меня тут для тебя такая вот диаграмма, чтобы ты понял, как это в структуре работает, и я задал, насколько крутой проект ты же сможешь написать. Вообще, наш AI будет супер твоим Python-помощником. У него будет возможность писать код за тебя. Кстати, не просто код, а целый проект. Он для тебя напишет всю структуру Python-проекта, requirements, Docker-файлы, Docker Compose, возможно, Bash-файлы, Bat-файлы и так далее.
Также он сможет эффективно писать парсеры на Python, так как у него будет возможность посещать сайты и анализировать их исходный код. Также он сможет эффективно писать парсеры на Python, так как у него будет возможность посещать сайты и анализировать их исходный код. Ещё дополнительная фича, чтобы был в курсе всех событий, он умеет пользоваться Google, ну, чтобы там гуглил всякую информацию, типа даты и времени, какая сейчас последняя версия Python и другие важные новости. Главное, он сможет запускать проекты, и если они не работают, он будет это сам понимать и исправлять свой код.
Вот тут у нас будет красивый веб-интерфейс. То есть, просто HTML-страничка. Тебе не нужно будет писать этот frontend, его за тебя уже написал. Чтобы ты не тратил своё время, видео про AI-агентов. Да, через этот веб-интерфейс ты будешь отправлять свои сообщения в backend. Да, да, мы будем разрабатывать свой мини-backend. Бояться этого не стоит, дружище, потому что там будет всего лишь две функции, чтобы чисто связать frontend со скриптом запросов на нейронку.
Сама же нейронка у нас будет на OpenAI. Да, я понимаю, что она стоит денег, но есть и бесплатный вариант. Ещё тебе понадобится VPN. Но именно для тебя, мой друг, у меня есть решение всех твоих проблем. Под роликом я оставлю ссылку на хороший VPN. Он, конечно, платный, но тебе дадут 3 дня бесплатного доступа. Думаю, должно хватить для разработки этого проекта. А насчёт того, чем можно заменить OpenAI, это, конечно же, Grok. Это не нейронка от Илона Маска, это платформа, которая даёт тебе ограниченный, но бесплатный доступ к Open Source моделям, таким как Llama и другие модели.
Насчёт кода, можешь даже не беспокоиться, потому что полностью поддерживается библиотека OpenAI, и тебе достаточно лишь заменить Base URL. Далее я покажу, как это сделать. Ну, а я на видео буду использовать модель GPT-4 Mini от OpenAI, потому что, по-моему, она стабильнее, быстрее, чем тот же GPT-3.5. И пропишет ей функции. Да, кстати, кто не знал, языковые модели могут вызывать функции и передавать в них правильные аргументы. По сути, все агенты используют именно возможность вызова функций, что и делает их многофункциональными.
Так, коротко пройдёмся по всем функциям, которые будет вызывать наша нейронка: это функция терминала для запуска Python-файлов, создание папок, директорий, файлов и так далее. Ну, вообще, всё, что можно сделать в терминале. Поиск в Google. Мы будем использовать SerpApi. Это сервис, который позволяет нам делать запросы в Google и получать информацию оттуда. Функция посещения веб-сайтов, чтобы получать структуру веб-страниц. Использовать мы будем библиотеку Playwright. Она типа как Selenium. И функция сохранения кода, чтобы она сохраняла код в нашей системе.
В целом, с функциями всё. Давайте расскажу про сервис Helicon. Этот сервис помогает мне смотреть статистику моих запросов. Удобнее, чем в платформе OpenAI. Вы можете её не использовать или использовать. В любом случае, я оставлю все ссылки в описании под роликом. В ней очень удобно смотреть, сколько стоил твой запрос и какой там был диалог с нейронкой. Вам нужно тут зарегистрироваться и получить свой ключ. Это можно сделать в настройках, вот здесь вот, и в разделе API Keys.
Ещё нам нужно зарегистрироваться в SerpApi. Вам дадут 2500 запросов бесплатно, чтобы делать запросы в Google. И там тоже нужно создать API-ключ и сохранить в коде. Это делается в настройках, вот здесь вот, и просто копируете. Ещё ключ от OpenAI. Ссылку тоже оставлю в описании. Здесь вот нажимаем "Создать ключ" и обязательно ставим разрешения ключа, чтобы было также. Для тех, кто хочет бесплатно использовать нейронку, для него я тоже оставлю ссылку на документацию Grok. Зарегистрируйтесь в платформе Grok и создаёте ключ. Он совершенно бесплатный, но есть лимиты. Вот сами можете посмотреть, какие тут нейронки и какие тут лимиты. Вот тут ключик создаёте. Чуть позже покажу, как интегрировать его.
Давайте для начала создадим папку для проекта. Я её назову Python AI. Откроем эту папку в нашем текстовом редакторе. У меня это VS Code, у вас может быть Visual Studio, Notepad или Sublime, или вообще блокнот или Nano. Версия Python у вас должна быть выше десятой или десятая. Создаём окружение и активируем.
[музыка]
Давайте теперь создадим основной файл. Назову его main.py. И давайте начнём с разработки бэкенда на FastAPI. Но сначала нам необходимо установить эту библиотеку. Сразу запишем её в requirements и установим. Ещё установим библиотеку Uvicorn, чтобы наш backend запускать. FastAPI установился. Давайте его импортируем и создадим объект App. Создадим основной роут, который для начала просто выведет "Hello, world!" и запустим наш backend через Uvicorn на порту 8000. Если у вас не индексируется в библиотеке, как у меня, вам нужно указать правильный Python из виртуального окружения. Вот так вот, и всё заработает. Запустим Uvicorn.
Круто, получаем ответ. Идём дальше. Под описанием ролика я оставлю ссылку на файл нашего чата. Создадим index.html и просто вставим его туда. Ничего делать на фронтенде не надо, всё уже настроено. И объяснять подробно я, в принципе, не буду, потому что урок у нас про AI-агентов. Поэтому нам сейчас нужно его просто выводить при запросе на backend, чтобы нам показывать HTML-страницу. Вместо JSON нам нужно импортировать класс `HTMLResponse` из FastAPI, прочитать HTML-файл и передать его как аргумент функции `HTMLResponse`. Давайте посмотрим, что из этого вышло. Крутой интерфейс показывается, сообщения отправляются, но пока что мы их не обрабатываем.
Я предлагаю это исправить. Наши сообщения между фронтендом и бэкендом передаются через WebSocket. И поэтому импортируем класс `WebSocket` из библиотеки FastAPI. Создаём роут `/ws` и вызовем функцию `websocket.accept()`, чтобы принимать [музыка] соединение. В цикле будем принимать сообщение с фронта. Добавим сообщение с фронта в переменную. Удалим в ней лишние пробелы. Также берём наш код в `try-except`, чтобы если что, отловить отсоединение с клиентом правильно. Для этого импортируем класс `WebSocketDisconnect` из [музыка] FastAPI. Давайте проверим. Получили ошибку. По умолчанию, FastAPI, кажется, не поддерживает сокеты, поэтому нужно установить дополнение `python-multipart`. Давайте это и [музыка] сделаем. Библиотека установилась. Давайте проверим. Как мы видим, мы получили сообщение. Давайте заменим принты на логирование через библиотеку `loguru`. Так они будут смотреться намного лучше. И импортируем `logger` из библиотеки `loguru`.
[музыка]
[музыка]
[музыка]
Далее создадим фейковый ответ от нейронки и отправим через сокеты. Для этого используем функцию `send_text`. И нам нужно будет это отправить в JSON-формате, поэтому импортируем библиотеку `json`. Конвертируем наш словарь и отправляем. Давайте теперь [музыка] проверим. Всё чётко работает.
Теперь нам нужно, чтобы реальная нейронка отвечала. Для этого устанавливаем библиотеку `openai`, чтобы делать запросы к Chat GPT. Импортируем асинхронный класс `OpenAI`. Создаём экземпляр класса и меняем `Base URL` на ссылку от Helicon, если мы хотим, чтобы все наши запросы проходили через них и они делали для нас подробную статистику. Вот такая вот ссылка у них. Также нужно в заголовках запроса указать ключ от Helicon. Но ключик хранить в `.env` не стоит, их нельзя хранить на компьютере или, как в нашем случае, в файле. То, давайте вставим сюда ключик. Также нам нужно будет создать файл `.env`, чтобы тут хранить настройки скрипта. И давайте ещё установим `python-dotenv`, чтобы импортировать в Python переменные из `.env` из библиотеки `dotenv`. Импортируем функцию `load_dotenv` и вызываем её таким образом. Она скопирует все файлы из `.env` в переменные среды, а из переменных сред уже получаем через библиотеку по ключу переменной. Ещё я импортировал ключ Helicon API из `.env` файла.
Если вы хотите использовать бесплатную нейронку, то я покажу вам, как это сделать через Grok. Переходим на платформу Grok, на этой странице получаем ключ. Добавим новую переменную `GROK_API_KEY` в коде. Меняем `Base URL` на вот такой вот. Headers не нужны, а ключ от Grok мы вставляем в параметр `api_key`. Но в этом видеоролике я буду использовать OpenAI, поэтому я оставлю Helicon, чтобы мои запросы проходили через них, я получал подробную статистику. В целом, код будет такой же, потому что Grok полностью совместим с библиотекой OpenAI, и можете кроме OpenAI использовать другие модели. Ладно, давайте обратно к коду. Уберём наш фейковый ответ и создадим список историй чата и сразу добавим туда системный промт. Временно напишу, что он просто понимает по Python, потом поменяем. Нам теперь нужно добавить сообщение пользователя в список и сделать запрос к нейронке с историей [музыка] чата. Будем использовать дисковую модель GPT-4 Mini и в параметре `messages` передадим историю чата. Так, сохраним сообщение, которое нам вернёт нейронка, и добавим её тоже в историю [музыка] чата. Вернём сообщение фронтенду и затестим.
Добавить в переменные среды ключ от [музыка] OpenAI. Важно, чтобы переменная называлась именно `OPENAI_API_KEY`. В таком случае его не нужно будет импортировать в код, библиотека сама его найдёт и будет использовать. Пробуем ещё раз. Получаем ошибку. Так, я разобрался, в чём была ошибка. Из `AIMessage` брать контент и выдавать его как текст. Проверим. Подозреваю, что ошибка у нас вот здесь вот. Давайте её быстро закомментируем. Системный промт. Потом вернёмся к решению этого бага. Запишем его в переменную `SYSTEM_PROMPT` в конфиге. Я его сейчас вручную писать не буду, просто скопирую и вставлю, что я его писал и тестировал ранее, а сейчас это бы заняло много времени. Поэтому переходим по ссылке в описании и даже копируем и вставляем. Главное, укажите данные вашего компьютера в системном промте. То есть, у меня это `id: dora`, операционная система `Linux`. У вас может быть `Ubuntu`, `Windows`. Терминал укажите, какой у вас. У меня это `bash`. У вас может быть `cmd` или `PowerShell`. Bash. Это нужно, чтобы нейронка делала правильные команды в терминале и также правильную версию Python, дефолтную, которая стоит у вас в [музыка] системе.
Проверим наш системный промт. Отлично, всё работает. Она поняла, что она должна делать. Давайте теперь пропишем функцию, которая будет вызывать нейронка. Для этого создадим новый файл `functions.py` и туда запишем наши функции. Начнём с функции сохранения кода. Принимать себя она будет два аргумента: `code` и `filename`. Для сохранения возвращать будет статус в виде строки. Также импортируем `logger`, чтобы понимать, что функция была вызвана. Обернёмся, что у нейронки память не сильно большая, а ошибка может быть огромной, поэтому давайте сократим её до 5000 символов с помощью библиотеки `os`. Создадим папку `ai`, если её не существует, и запишем туда файл с кодом. Давайте [музыка] проверим. Функция успешно вызвалась и создала файл. Круто. Дальше писать следующую функцию. И это будет функция работы с терминалом. Назовём её `run_command`. Будет себя три параметра: `command` — это команда для запуска, `input_str` — для ввода каких-либо данных, если скрипт требует ввода. Для этой функции мы воспользуемся библиотекой `subprocess` и будем вызывать функцию `Popen`. И укажем, что все команды будут изначально в папке происходить. И пропишем условие, что если будут какие-либо входные данные, то их тоже вводим. Будем получать ошибку и успешных сообщений с терминала при исполнении. Здесь тоже будем возвращать ограниченное количество символов, ибо ответ тоже может быть непредсказуемым и может превышать контекст нейронки.
Далее, давайте создадим функцию поиска информации с Google. Назовём её `search`. Принимать будет параметр `query`. И сразу обернёмся к `try-except`. Нам понадобится библиотека `requests`, чтобы отправлять запросы. Судя по документации, нужно делать запрос на вот этот URL и в заголовках запроса указываем ключ SerpApi. Гайды по SerpApi и в личном кабинете получаем API-ключ и вставляем в `.env` файл. Его импортируем в `config` и из `config` уже импортируем `functions.py`. А в теле запроса указываем наш `query`. Давайте результат запишем в переменную. И если статус запроса 200 или 201, то возвращаем результат. Давайте проверим функцию. Напишем, допустим, "Кто был первым президентом Соединённых Штатов Америки?" и выведем результат через логгер. Запускаем. Результат получен. Всё отлично. Только не совсем понятно. Давайте посмотрим это в отформатированном [музыка] формате. В целом, думаю, всего этого текста будет слишком много. Можем, можем выдавать нейронке только первые три элемента в списке `organic_results` и брать из них только `title` и `snippet`. Давайте это и сделаем. Чтобы преобразовать список в строку, будем использовать функцию [музыка] `join`. И вот в таком формате будем возвращать нейронке. Проверяем. Всё хорошо. Я думаю, так понятнее будет для нейронки.
Давайте теперь создадим функцию, которая будет открывать сайт по URL. Назовём её `fetch_page`. Как параметр у неё будет `url`, и возвращать будет тоже строку. Обернём это тоже в `try-except`. Для открытия веб-страниц мы будем использовать библиотеку `Playwright`. Её необходимо установить через `pip`. Также нам понадобится библиотека `playwright-stealth`, потому что обычная библиотека `Playwright` помечается сайтами как бот, и чтобы избежать этого, мы будем использовать дополнение `playwright-stealth`. Из `playwright` импортируем `async_playwright`. Из библиотеки `playwright-stealth` импортируем `stealth`. `async_playwright` создаём браузер на базе Chromium с параметрами `headless=False`. Это позволяет нам видеть браузер, когда он создаётся. И в `args` передаём параметры для отключения свойства того, что браузер управляется скриптом. Далее создаём новый контекст браузера с `user_agent`. По сути, это является отдельной изолированной средой внутри одного экземпляра браузера. Это похоже на режим инкогнито, где cookies и локальное хранилище изолированы, что позволяет работать с несколькими сеансами параллельно, не влияя друг на друга. Также зададим ширину и высоту браузера. Откроем новую вкладку в браузере. Применим функцию `stealth`, чтобы замаскировать нашего браузера. Скриптом с помощью функции `goto` идём по ссылке. Ждём, пока страница загрузится. Сделаем скриншот для дебага. Укажем путь скриншота. И также нам ещё понадобится `BeautifulSoup`. Это библиотека для парсинга веб-страниц. Импортируем её. Давайте сохраним исходный код страницы, которую мы посетили. Создадим экземпляр класса `BeautifulSoup`, передадим туда HTML-страницу и укажем режим парсинга `html.parser`. Пройдёмся по всем тегам в HTML-структуре и удалим оттуда `script`, `style` теги и `iframe`, потому что по сути они не нужны для нейронки, и зачем нам заполнять ею её контекст и память. Также удалим атрибут `style` во всех тегах, чтобы они тоже не заполняли [музыка] страницу. Поэтому будем возвращать максимум 20000 символов. Давайте проверим нашу функцию. Она асинхронная, поэтому нам понадобится `async` чтобы запустить её. Создадим функцию `main` и вызовем. Ошибочка, скорее всего, мне необходимо установить `setuptools`. Видимо, это зависимость `stealth` Playwright, поэтому устанавливаем и проверим ещё. Если вы впервые установили Playwright, вам необходимо прописать `playwright install`, чтобы она установила свои браузеры, которые нужны для работы. Давайте теперь проверим. Вы получили ошибку. Это из-за того, что я не прописал для функции `fetch_page` и ещё я забыл сделать `fetch_page` асинхронные. Давайте исправим это и попробую открыть свой веб-сайт. Не совсем правильную структуру получил. Сейчас попробую разобраться. Друзья, я также заменил ссылку для теста на `example.com`, ибо, оказывается, у меня была проблема на своём сайте, и она неправильно открывалась. Давайте проверим `example.com` и видим то, что мы получаем отфильтрованный, красивую HTML-страницу. Отлично, функция работает. Идём дальше.
А дальше нам необходимо написать описание всем этим функциям, чтобы нейронка понимала, как их вызывать, какие аргументы передавать им и как они вообще называются. Вот документация OpenAI. Я ссылку тоже в описании оставлю, можете посмотреть. В принципе, тут вот название функции, параметры, её какой у неё тип, описание. Всё, что вы здесь описываете, идёт в системный промт. На самом деле, вот и описание, на самом деле, тоже не может быть очень большим, потому что могут превышать лимиты. Давайте расскажу вам алгоритм работы `function_calling`. Первое — происходит определение функции. Мы заранее прописываем нейронке, какие у неё есть функции и какие у них параметры, и какие у неё параметры, какие могут быть значения. Второй шаг — анализ запроса. Нейронная сеть получает запрос пользователя и анализирует его на наличие задач, которые можно решить вызовом одной из этих функций. Например, когда пользователь в нашем случае напишет "Создай файл main.py", она анализирует запрос пользователя, смотрит на свои функции и видит то, что там есть функция создания файла и возвращает серверу ответ в виде JSON с именем функции и нужными аргументами для запуска этой функции. Далее мы уже на стороне сервера вызываем эту функцию и передаём туда аргументы. Функция выполняется, возвращается результат, и этот результат мы можем показать сразу пользователю. Либо мы можем отправить это нейронке и сделать генерацию ответа ещё раз, чтобы чтобы нейронка сама сделала какой-то свой ответ. В нашем случае результат функции мы будем отправлять нейронке.
Давайте в переменной укажем свои функции. Просто скопируем структуру функции из документации OpenAI. Поменяем описание, название и параметры. Первая функция будет `run_command`. Напишу короткое описание, что эта функция выполняет команды в терминале. Укажем наши параметры и их описание. Тип параметра будет `string`. Тип параметра `input_str` у нас тоже будет `string`. Вот этот массив `required` он означает то, что вот эти аргументы они обязательны и без них запускать нельзя. Также я где-то видел то, что можно указать тип ответа функции. Давайте попробуем. Напишу короткое описание. Вторая функция у нас будет `save_code`. Напишем им тоже короткое описание: "Создаёт файлы с содержимым и сохраняет код в директории". Поменяем параметры. У нас это будет `code` и `filename`. Description тоже поменяем: "Содержание файла", "Имя файла". И `required` тоже поменяем: `code` и `filename`. Возвращая, наверное, уберу здесь, это не обязательно. Аналогично выполнение для функции `search`. Короткое описание: "Выполняет запрос поиска Google". Тут у нас уже параметры `query`. [музыка] И последняя функция `fetch_page`. Напишем ей короткое [музыка] описание: "Параметр URL". [музыка] С функциями мы закончили. Вернёмся к `main.py`. Напишем цикл для того, чтобы нейронка постоянно проверяла сама себя до тех пор, пока она не поймёт, что задание выполнено. Допустим, пользователь даёт задание нейронке написать код для работы с каким-то API. Шаг первый для нейронки будет вызов функции создания файла. Если статус выполнения функций был успешным, и нейронка считает, что дальше не нужно выполнять никаких функций, и этого достаточно, то она возвращает уже обычное сообщение без вызова функции, и в этот момент мы выходим из цикла. В других случаях мы будем отправлять статусы вызова функции нейронке до тех пор, пока она не посчитает, что больше не нужно вызывать функции, и всё готово. Давайте импортируем `Tools` из `openai` и передадим в аргумент `tools` и параметр `tool_choice`. Означает "вызывать ли функцию постоянно" либо "по мнению AI". Если ставить `auto`, то функция будет вызываться только если AI считает это нужным. Параметр `parallel_tools` ставим `True`. Это означает то, что нейронка может вызывать сразу несколько функций одновременно. [музыка] Давайте раскомментируем. Нет ли вызова функции в сообщении нейронки? И в таком случае вернём её обычное сообщение и выйдем из цикла. Пройдёмся по функциям, которую вызвала нейронка, получим название функции и спарсим её аргументы. Заранее создадим переменную `result`. Обернёмся в `try-except` ошибку. Дальше будем сравнивать название функции с нашими функциями и просто вызывать их с аргументами. Не забываем импортировать наши функции из файла [музыка] `functions.py`. Сделаем здесь небольшое условие, так как `input_str` у нас не обязательный параметр. Если `input_str` есть в аргументах, то называем функцию с `input_str`, в противном случае без неё. Результат функции будем записывать [музыка] в `result`. Ещё проще здесь, из-за того, что нейронка может вызывать и несуществующие функции, поэтому мы это тоже будем обрабатывать. Запишем просто, что была вызвана неизвестная функция. В конце цикла в историю диалога добавим результат функции. Роль у нас будет `tool`, контент — это результат функции, `call_id` — это ID вызова функции. Это обязательно, его всегда нужно ставить. В целом, наверное, всё. Давайте проверим. Привет, напиши мне проект для сокращения ссылок. У нас ошибка `str`. Скорее всего, `list_of_str`, он требует, чтобы все аргументы были заполнены, поэтому давайте её уберём и протестируем без него. Как мы видим, нейронка вызывает функции, создаёт файлы `requirements.txt`, создала `Dockerfile`, и в целом написала весь проект с несколькими файлами. Но единственное, то, что я вижу, то, что она не создала отдельную папку для проекта. Сообщение видим, код видим. И давайте это исправим. Мне не нравится то, что она не создаёт папку для проекта, потому что проектов может быть много, и в одной папке они не могут располагаться. Давайте исправим это в системном промте. Друзья, я исправил системный промт. У вас он уже будет исправленный, потому что вы его в начале видеоролика скопируете с моего репозитория. Ссылка тоже будет в описании. И давайте ещё раз проверим. Подождём, пока отработает наш запрос. Нейронка [музыка] создаёт файлы в папке `URL`. Но видит то, что такой папки не существует, понимает это и создаёт эту папку, и уже далее создаёт файлы внутри этой папки. Также она запускает установку зависимостей. Вернула нам результат, говорит то, что проект был успешно создан. Давайте попросим её без бэкенда реализовать это и проверить на работоспособность. Создаются новые файлы. Скрипт `scripts.py` был успешно создан. Вот что он делает. Угу, круто, всё работает. Давайте теперь попросим его сделать парсер. Напиши новый проект парсинга страницы `example.com`. [музыка] Создаёт новую папку `web_parser`. Успешно сгенерировал проект, установил зависимости. Но мне кажется, он не проверил. Давайте спросим его теперь. Он проверяет. Да, запускает файл. И какой же результат мы получим? Скрипт успешно выполнен. Вот результат: ссылки на страницы. Давайте дадим ему новое задание, чтобы он спарсил весь текст, который на сайте, и [музыка] посмотрим. У нас происходит функция `fetch_page`, и нейронка выдаёт результат. Но почему-то она в чате выдала. Давайте попросим её, чтобы она сгенерировала скрипт для этого, создала новый файл, проверила её, добавила комментарий к файлу, и в целом со своей задачей она справилась. И на этом всё, братья мои. Если тебе понравилось, поставь лайк. А если не понравилось, поставь дизлайк. Расскажи в комментариях, что тебе не понравилось, чтобы я это исправил. Всем пока! Всем большое спасибо за просмотр!