Перейти к основному содержимому

Tool Calling @ Yandex Cloud with API, часть 2

Tool Calling, мотивация с точки зрения бизнеса
Интерактивные ноутбук и полезные утилиты, использованные в руководстве.
Код покрыт тестами и поддерживается.

Создаем ассистента для онлайн-маркетплейса

Продолжаем разработку нашего бота-ассистента, который будет помогать пользователю на онлайн-маркетплейсе. Пайплайн, по которому мы хотим пройти, как пользователь:

  1. Получить баланс пользователя
  2. Получить список товаров, которые мы можем купить, укладываясь в бюджет
  3. Оформить заказ

Давайте создадим несколько инструментов, которые нам пригодятся для нашего бота-ассистента.

  • balance_tool - инструмент для получения баланса пользователя по его userId
  • product_list_tool - инструмент для получения списка товаров с фильтрациями и сортировками
  • order_tool - инструмент для создания заказа по productId
balance_tool = {
"function": {
"name": "BalanceTool",
"description": "Получает баланс пользователя в рублях из базы данных.",
"parameters": {
"type": "object",
"properties": {
"userId": {
"type": "string",
"description": "Идентификатор пользователя, для которого нужно получить баланс."
}
},
"required": ["userId"],
"additionalProperties": False
}
}
}
Далее в коде я буду использовать функции:
  • chat_completion_request(messages, tools=None) -> YandexGPTResponse - функция для отправки запроса к YandexGPT
  • get_function_call_from_response(response) -> toolCalls - функция для получения объекта toolCalls из ответа YandexGPT
  • process_functions(toolCalls) -> toolResult - функция, которая распознает, какую питон-функцию вызвать, и запускает ее, передавая ей аргументы от LLM
Подробнее про process_functions
def process_functions(toolCalls: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Обрабатывает список вызовов инструментов и возвращает результаты."""
tools_map = {
"ProductListTool": process_tool_product_list_tool,
"BalanceTool": process_tool_balance_tool,
"OrderTool": process_tool_order_tool,
"ask_database": process_tool_sql_tool,
"searchapi": process_tool_searchapi,
"serverless_func": process_tool_serverless_func
}

results = []
for tool_call in toolCalls:
name = tool_call['functionCall']['name']
arguments = tool_call['functionCall'].get('arguments', {})
if name in tools_map:
# Вызываем функцию с переданными аргументами
result = tools_map[name](**arguments)
results.append(pack_function_result(name, result))

return results

def process_tool_product_list_tool(category: str, priceMinMax: List[int], sortBy: str = 'price') -> List[Dict[str, Any]]:
"""Обрабатывает запрос на получение списка продуктов."""
# Здесь можно добавить логику для получения реальных данных
return [
{'productId': 'siaomi453', 'name': 'сяоми 4+ pro', 'category': category, 'price': 10_000},
{'productId': 'iphone15', 'name': 'iphone 15', 'category': category, 'price': 280_000}
]

def process_tool_balance_tool(userId: str) -> Dict[str, Any]:
"""Обрабатывает запрос на получение баланса пользователя."""
# Здесь можно добавить логику для получения реального баланса
return {'userId': userId, 'balance': 10000}

def process_tool_order_tool(productId: str, quantity: int) -> Dict[str, Any]:
"""Обрабатывает запрос на создание заказа."""
# Здесь можно добавить логику для обработки заказа
return {'orderId': 'order123', 'productId': productId, 'quantity': quantity}

def process_tool_sql_tool(query: str) -> List[tuple]:
"""Обрабатывает SQL-запрос и возвращает результаты."""
conn = None
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute(query)
result = cursor.fetchall()
return result
except sqlite3.Error as e:
print(f"Ошибка при выполнении SQL-запроса: {e}")
return []
finally:
if conn:
conn.close()

def process_tool_searchapi(query: str) -> str:
"""Обрабатывает запрос к поисковому API."""
return search_api_generative(query)

def process_tool_serverless_func(name: str) -> str:
"""Обрабатывает вызов серверной функции."""
return send_post_request(name)

Простой вызов функции

Давайте начнем с простого: получим баланс пользователя.

session_info = "\n\nИнформация о сессии:\nТекущее время: 12:11. Текущий userId пользователя: wg359g3f39"

messages = []
messages.append({"role": "system", "text": f"Ты - полезный бот, который помогает пользователю с его проблемами. Ты можешь использовать инструменты, чтобы получить актуальную информацию. Но пользоваться ими нужно не всегда. {session_info}"})
messages.append({"role": "user", "text": "Сколько у меня денег на балансе?"})

response = chat_completion_request(messages, tools=[balance_tool])

toolCalls: list = get_function_call_from_response(response)
messages.append({
'role': 'assistant',
'toolCallList': {'toolCalls': toolCalls}
})

pretty_print_conversation(messages)
system: Ты - полезный бот, который помогает пользователю с его проблемами. Ты можешь использовать инструменты, чтобы получить актуальную информацию. Но пользоваться ими нужно не всегда. 
Информация о сессии:
Текущее время: 12:11. Текущий userId пользователя: wg359g3f39

user: Сколько у меня денег на балансе?

assistant: {'toolCalls': [{'functionCall': {'name': 'BalanceTool', 'arguments': {'userId': 'wg359g3f39'}}}]}

Выполним функцию, и вернем результат обратно gpt. Используем заранее подготовленную функцию process_functions, внутри которой вызовется функция process_tool_balance_tool с аргументом userId

toolResults: list = process_functions(toolCalls)
messages.append({
'role': 'assistant',
'toolResultList': {'toolResults': toolResults}
})

response = chat_completion_request(messages, tools=[balance_tool])
messages.append(response['result']['alternatives'][0]['message'])
pretty_print_conversation(messages)
system: Ты - полезный бот, который помогает пользователю с его проблемами. Ты можешь использовать инструменты, чтобы получить актуальную информацию. Но пользоваться ими нужно не всегда. 
Информация о сессии:
Текущее время: 12:11. Текущий userId пользователя: wg359g3f39

user: Сколько у меня денег на балансе?

assistant: {'toolCalls': [{'functionCall': {'name': 'BalanceTool', 'arguments': {'userId': 'wg359g3f39'}}}]}

assistant: {'toolResults': [{'functionResult': {'name': 'BalanceTool', 'content': "{'userId': 'wg359g3f39', 'balance': 10000}"}}]}

assistant: На вашем балансе 10000 рублей.

Как можно видеть, LLM прочитала то, что мы отправили ей в toolResults, и вывела результат на человеческом языке.

Параллельный вызов нескольких функций

Что имеется в виду под параллельным вызовом

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

Представим другую ситуацию:

Например, у бота есть инструмент read_page_by_url - прочитать контент страны по ссылке. Пользователь просит суммаризировать контент с 5 разных сайтов. С последовательным вызовом нам придется ждать, пока LLM последовательно сделает 5 вызовов, и только потом ответит пользователю.

В этом случае гораздо лучше прочитать 5 сайтов за 1 цикл ToolCall'а: вызвать этот инструмент 5 раз в одном запросе и получить 5 ответов, и потом сразу отвечать пользователю. Так выглядит параллельное использование инструментов.

Теперь давайте попробуем за 1 цикл tool calling_a получить список товаров, которые нам по карману:

User prompt: ...которые я могу себе позволить купить с учетом моего баланса. Показывай только те товары, которые я могу себе позволить купить.

Для этого нам одновременно нужно передать в LLM и ассортимент, и наш баланс.

session_info = "\n\nИнформация о сессии:\nТекущее время: 12:11. Текущий userId пользователя: wg359g3f39"
tools = [product_list_tool, balance_tool, order_tool]

messages = []
messages.append({"role": "system", "text": f"Ты - полезный бот, который помогает пользователю с его проблемами. Ты можешь использовать инструменты, чтобы получить актуальную информацию. Но пользоваться ими нужно не всегда. Если пользователь ищет товары, то всегда параллельно с вызовом инструмента ProductListTool вызывай и BalanceTool. {session_info}"})
messages.append({"role": "user", "text": "Покажи мне товары категории 'электроника' в диапазоне цен от 1000 до 50000 рублей, отсортированные по популярности, которые я могу себе позволить купить с учетом моего баланса. Показывай только те товары, которые я могу себе позволить купить."})

response = chat_completion_request(messages, tools=tools)

toolCalls: list = get_function_call_from_response(response)
messages.append({
'role': 'assistant',
'toolCallList': {'toolCalls': toolCalls}
})

pretty_print_conversation(messages)
system: Ты - полезный бот, который помогает пользователю с его проблемами. Ты можешь использовать инструменты, чтобы получить актуальную информацию. Но пользоваться ими нужно не всегда. Если пользователь ищет товары, то всегда параллельно с вызовом инструмента ProductListTool вызывай и BalanceTool. 
Информация о сессии:
Текущее время: 12:11. Текущий userId пользователя: wg359g3f39

user: Покажи мне товары категории 'электроника' в диапазоне цен от 1000 до 50000 рублей, отсортированные по популярности, которые я могу себе позволить купить с учетом моего баланса. Показывай только те товары, которые я могу себе позволить купить.

assistant: {'toolCalls': [{'functionCall': {'name': 'ProductListTool', 'arguments': {'category': 'электроника', 'priceMinMax': [1000, 50000], 'sortBy': 'popularity'}}}, {'functionCall': {'name': 'BalanceTool', 'arguments': {'userId': 'wg359g3f39'}}}]}

LLM вызвал оба инструмента параллельно. Давайте передадим оба вызова в process_functions, которая выполнит оба инструмента и вернет результаты. После этого оба результата пошлем в LLM.

toolResults: list = process_functions(toolCalls)
messages.append({
'role': 'assistant',
'toolResultList': {'toolResults': toolResults}
})

response = chat_completion_request(messages, tools=tools)
messages.append(response['result']['alternatives'][0]['message'])
pretty_print_conversation(messages)
system: Ты - полезный бот, который помогает пользователю с его проблемами. Ты можешь использовать инструменты, чтобы получить актуальную информацию. Но пользоваться ими нужно не всегда. Если пользователь ищет товары, то всегда параллельно с вызовом инструмента ProductListTool вызывай и BalanceTool. 
Информация о сессии:
Текущее время: 12:11. Текущий userId пользователя: wg359g3f39

user: Покажи мне товары категории 'электроника' в диапазоне цен от 1000 до 50000 рублей, отсортированные по популярности, которые я могу себе позволить купить с учетом моего баланса. Показывай только те товары, которые я могу себе позволить купить.

assistant: {'toolCalls': [{'functionCall': {'name': 'ProductListTool', 'arguments': {'category': 'электроника', 'priceMinMax': [1000, 50000], 'sortBy': 'popularity'}}}, {'functionCall': {'name': 'BalanceTool', 'arguments': {'userId': 'wg359g3f39'}}}]}

assistant: {'toolResults': [{'functionResult': {'name': 'ProductListTool', 'content': "[{'productId': 'siaomi453', 'name': 'сяоми 4+ pro', 'category': 'электроника', 'price': 10000}, {'productId': 'iphone15', 'name': 'iphone 15', 'category': 'электроника', 'price': 280000}]"}}, {'functionResult': {'name': 'BalanceTool', 'content': "{'userId': 'wg359g3f39', 'balance': 10000}"}}]}

assistant: Вы можете себе позволить купить следующие товары:

1. **Сяоми 4+ pro**
- **Категория:** Электроника
- **Цена:** 10000 рублей

Ваш баланс составляет 10000 рублей, что позволяет вам приобрести данный товар.

Оформление заказа

Теперь закончим наш диалог, приобретя товар.

messages.append({"role": "user", "text": 'Как пелось у RX4D в "запрети мне носить сяоми", это лучший микрокомпьютер, топ за свои деньги. Оформи заказ на сяоми 4+'})

response = chat_completion_request(messages, tools=tools)

toolCalls: list = get_function_call_from_response(response)
messages.append({
'role': 'assistant',
'toolCallList': {'toolCalls': toolCalls}
})

toolResults: list = process_functions(toolCalls)
messages.append({
'role': 'assistant',
'toolResultList': {'toolResults': toolResults}
})

response = chat_completion_request(messages, tools=tools)
messages.append(response['result']['alternatives'][0]['message'])
pretty_print_conversation(messages)
system: Ты - полезный бот, который помогает пользователю с его проблемами. Ты можешь использовать инструменты, чтобы получить актуальную информацию. Но пользоваться ими нужно не всегда. Если пользователь ищет товары, то всегда параллельно с вызовом инструмента ProductListTool вызывай и BalanceTool. 
Информация о сессии:
Текущее время: 12:11. Текущий userId пользователя: wg359g3f39

user: Покажи мне товары категории 'электроника' в диапазоне цен от 1000 до 50000 рублей, отсортированные по популярности, которые я могу себе позволить купить с учетом моего баланса. Показывай только те товары, которые я могу себе позволить купить.

assistant: {'toolCalls': [{'functionCall': {'name': 'ProductListTool', 'arguments': {'category': 'электроника', 'priceMinMax': [1000, 50000], 'sortBy': 'popularity'}}}, {'functionCall': {'name': 'BalanceTool', 'arguments': {'userId': 'wg359g3f39'}}}]}

assistant: {'toolResults': [{'functionResult': {'name': 'ProductListTool', 'content': "[{'productId': 'siaomi453', 'name': 'сяоми 4+ pro', 'category': 'электроника', 'price': 10000}, {'productId': 'iphone15', 'name': 'iphone 15', 'category': 'электроника', 'price': 280000}]"}}, {'functionResult': {'name': 'BalanceTool', 'content': "{'userId': 'wg359g3f39', 'balance': 10000}"}}]}

assistant: Вы можете себе позволить купить следующие товары:

1. **Сяоми 4+ pro**
- **Категория:** Электроника
- **Цена:** 10000 рублей

Ваш баланс составляет 10000 рублей, что позволяет вам приобрести данный товар.

user: Как пелось у RX4D в "запрети мне носить сяоми", это лучший микрокомпьютер, топ за свои деньги. Оформи заказ на сяоми 4+

assistant: {'toolCalls': [{'functionCall': {'name': 'OrderTool', 'arguments': {'productId': 'siaomi453', 'quantity': 1}}}]}

assistant: {'toolResults': [{'functionResult': {'name': 'OrderTool', 'content': "{'orderId': 'order123', 'productId': 'Сяоми 4+ pro', 'quantity': 1}"}}]}

assistant: Ваш заказ на микрокомпьютер Сяоми 4+ успешно оформлен. Идентификатор заказа: order123.

Поздравляю! Ваш заказ на микрокомпьютер Сяоми 4+ успешно оформлен.

Вызов функции с нестрогими параметрами

Теперь попробуем реализовать приложение Chat-with-Data. Я заранее подготовил sqlite базу данных с таблицей products, в которой хранятся данные о товарах. Я хочу получить все товары категории 'электроника', отсортированные по рейтингу, ценой меньше 30к.

products_table_description = """
Таблица "products" предназначена для хранения информации о товарах, доступных в магазине.
Каждый товар имеет следующие поля:
- productId: уникальный идентификатор товара (TEXT, PRIMARY KEY)
- name: название товара (TEXT, NOT NULL)
- category: категория товара (TEXT, NOT NULL). Например: электроника, одежда, книги, дом, ягоды, сад
- price: цена товара (REAL, NOT NULL)
- num_of_orders: количество заказов данного товара (INTEGER, NOT NULL)
- rating: рейтинг товара (REAL, NOT NULL)
"""

sql_tool = {
"function": {
"name": "ask_database",
"description": f"Use this function to answer user questions about the database. Input should be a fully formed SQL query.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": f"""
SQL query extracting info to answer the user's question.
SQL should be written using this database schema:
{products_table_description}
The query should be returned in plain text, not in JSON.
""",
}
},
"required": ["query"],
},
}
}
session_info = "\n\nИнформация о сессии:\nТекущее время: 12:11. Текущий userId пользователя: wg359g3f39"
tools = [sql_tool, balance_tool, order_tool]

messages = []
messages.append({"role": "system", "text": f"Ты - полезный бот, который помогает пользователю с его проблемами. Ты можешь использовать инструменты, чтобы получить актуальную информацию. Но пользоваться ими нужно не всегда. {session_info}"})
messages.append({"role": "user", "text": "Мне нужны все товары категории 'электроника', отсортированные по рейтингу, ценой меньше 30к."})

response = chat_completion_request(messages, tools=tools)

toolCalls: list = get_function_call_from_response(response)
messages.append({
'role': 'assistant',
'toolCallList': {'toolCalls': toolCalls}
})

toolResults: list = process_functions(toolCalls) # внутри функции выполняется запрос к базе данных, написанный GPT
messages.append({
'role': 'assistant',
'toolResultList': {'toolResults': toolResults}
})

response = chat_completion_request(messages, tools=tools)
messages.append(response['result']['alternatives'][0]['message'])
with open('output.md', 'w', encoding='utf-8') as f:
assistant_message = response['result']['alternatives'][0]['message']['text']
f.write(assistant_message)

pretty_print_conversation(messages)
system: Ты - полезный бот, который помогает пользователю с его проблемами. Ты можешь использовать инструменты, чтобы получить актуальную информацию. Но пользоваться ими нужно не всегда. 
Информация о сессии:
Текущее время: 12:11. Текущий userId пользователя: wg359g3f39

user: Мне нужны все товары категории 'электроника', отсортированные по рейтингу, ценой меньше 30к.

assistant: {'toolCalls': [{'functionCall': {'name': 'ask_database', 'arguments': {'query': "SELECT * FROM products WHERE category = 'электроника' AND price < 30000 ORDER BY rating DESC"}}}]}

assistant: {'toolResults': [{'functionResult': {'name': 'ask_database', 'content': "[('1', 'Смартфон XYZ', 'электроника', 19999.99, 150, 4.5), ('5', 'Планшет JKL', 'электроника', 24999.99, 100, 4.3), ('3', 'Наушники DEF', 'электроника', 2999.99, 200, 4.2)]"}}]}

assistant: Вот список всех товаров категории 'электроника', отсортированных по рейтингу, ценой меньше 30к:

1. Смартфон XYZ
- Название: Смартфон XYZ
- Категория: электроника
- Цена: 19999.99 рублей
- Количество заказов: 150
- Рейтинг: 4.5

2. Планшет JKL
- Название: Планшет JKL
- Категория: электроника
- Цена: 24999.99 рублей
- Количество заказов: 100
- Рейтинг: 4.3

3. Наушники DEF
- Название: Наушники DEF
- Категория: электроника
- Цена: 2999.99 рублей
- Количество заказов: 200
- Рейтинг: 4.2

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

Дополнительно

Вызов Yandex Cloud Serverless Functions

Yandex Cloud Serverless Functions — это гибкая платформа, на которой можно запускать исполнение (py, js, go, etc.) функций без постоянного хостинга сервера. Для демонстрации интеграции я подготовил простенькую питон-функцию, которая принимает на вход только один параметр — name, и возвращает отформатированную строку с приветствием.

send_post_request & index.py

Функция в на моём компьютере:

def send_post_request(name):
url = f"https://functions.yandexcloud.net/d4ekuoccv9lmm79dh5rr?name={name}"
with httpx.Client() as client:
response = client.get(url)

return response.text

Функция в Yandex Cloud:

def handler(event, context):
name = event['queryStringParameters']['name']

return {
'statusCode': 200,
'headers': {
'Content-Type': 'text/plain'
},
'isBase64Encoded': False,
'body': 'Hello to {} from serverless func!'.format(name)
}

Давайте попросим GPT отправить к ней запрос.

serverless_func_tool = {
"function": {
"name": "serverless_func",
"description": "Запускает функцию на Yandex Cloud Serverless Functions",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Имя пользователя"
}
},
"required": ["name"],
},
}
}

tools = [serverless_func_tool]

messages = []
messages.append({"role": "user", "text": "Запусти функцию в yandex cloud, передай имя 'David'"})

# ... тот же код, что и в предыдущем примере
user: Запусти функцию в yandex cloud, передай имя 'David'

assistant: {'toolCalls': [{'functionCall': {'name': 'serverless_func', 'arguments': {'name': 'David'}}}]}

assistant: {'toolResults': [{'functionResult': {'name': 'serverless_func', 'content': 'Hello to David from serverless func!'}}]}

Использование Search API (Нейро поиск) в качестве инструмента

Код функции для отправки запроса в Нейро

searchapi_tool = {
"function": {
"name": "searchapi",
"description": "Отправляет запрос в поисковый движок по интернету Нейро и получает ответ",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Запрос для поиска в интернете"
}
},
"required": ["query"],
},
}
}

tools = [searchapi_tool]

messages = []
messages.append({"role": "system", "text": f"Ты - полезный бот, который помогает пользователю с его проблемами. Старайся по возможности использовать инструмент searchapi, чтобы получить информацию из интернета."})
messages.append({"role": "user", "text": "Погода в Москве"}) # если сделать запрос на "Свежие новости за сегодня", то скорее всего YandexGPT не сможет ответить, так как новости будут содержать острополитические темы, из-за которых запрос улетит в этику

# ... тот же код, что и в предыдущем примере
system: Ты - полезный бот, который помогает пользователю с его проблемами. Старайся по возможности использовать инструмент searchapi, чтобы получить информацию из интернета.

user: Погода в Москве

assistant: {'toolCalls': [{'functionCall': {'name': 'searchapi', 'arguments': {'query': 'Погода в Москве'}}}]}

assistant: {'toolResults': [{'functionResult': {'name': 'searchapi', 'content': 'Ответ SearchAPI:\n**Погода в Москве на 28 ноября 2024 года:**\n* 20:31: температура +1\xa0°C, пасмурно, влажность 88%, ветер 2 м/с, вероятность осадков 0%. [2]\n* 23:00: температура -1\xa0°C, пасмурно, влажность 89%, ветер 2 м/с. [2]\n\n**Прогноз на ближайшие дни:**\n* 29 ноября: утро — температура +1\xa0°C, дождь со снегом, влажность 91%, давление 755 мм, ветер 3 м/с, вероятность осадков 40%. [2] День — температура +2\xa0°C, пасмурно, влажность 90%, давление 756 мм, ветер 3 м/с, вероятность осадков 0%. [2] Вечер — температура +1\xa0°C, облачно с прояснениями, влажность 89%, давление 757 мм, ветер 2 м/с, вероятность осадков 0%. [2] Ночь — температура 0\xa0°C, пасмурно, влажность 90%, давление 753 мм, ветер 2 м/с, вероятность осадков 0%. [2]\n* 30 ноября: утро — температура +1\xa0°C, пасмурно, влажность 89%, давление 758 мм, ветер 4 м/с, вероятность осадков 0%. [2] День — температура +2\xa0°C, пасмурно, влажность 87%, давление 758 мм, ветер 4 м/с, вероятность осадков 0%. [2] Вечер — температура +1\xa0°C, пасмурно, влажность 88%, давление 759 мм, ветер 3 м/с, вероятность осадков 0%. [2] Ночь — температура +1\xa0°C, пасмурно, влажность 88%, давление 758 мм, ветер 3 м/с, вероятность осадков 0%. [2]\n\nБолее подробный прогноз погоды можно посмотреть на сайтах yandex.ru и weather.rambler.ru. [2][5]\n\nИсточники:\nhttps://yandex.ru/pogoda/moscow\nhttps://yandex.ru/pogoda/213?lat=55.7559&lon=37.6177\nhttps://meteoinfo.ru/forecasts/russia/moscow-area/moscow\nhttps://www.gismeteo.ru/weather-moscow-4368/\nhttps://weather.rambler.ru/v-moskve/\nhttps://world-weather.ru/pogoda/russia/moscow/'}}]}

assistant: Вот информация о погоде в Москве на 28 ноября 2024 года:

- **Сейчас:** температура +1 °C, пасмурно, влажность 88%, ветер 2 м/с, вероятность осадков 0%.
- **Ночью:** температура 0 °C, пасмурно, влажность 90%, давление 753 мм, ветер 2 м/с, вероятность осадков 0%.

**Прогноз на ближайшие дни:**

- 29 ноября: утро — температура +1 °C, дождь со снегом, влажность 91%, давление 755 мм, ветер 3 м/с, вероятность осадков 40%. День — температура +2 °C, пасмурно, влажность 90%, давление 756 мм, ветер 3 м/с, вероятность осадков 0%. Вечер — температура +1 °C, облачно с прояснениями, влажность 89%, давление 757 мм, ветер 2 м/с, вероятность осадков 0%. Ночь — температура 0 °C, пасмурно, влажность 90%, давление 758 мм, ветер 2 м/с, вероятность осадков 0%.

- 30 ноября: утро — температура +1 °C, пасмурно, влажность 89%, давление 758 мм, ветер 4 м/с, вероятность осадков 0%. День — температура +2 °C, пасмурно, влажность 87%, давление 758 мм, ветер 4 м/с, вероятность осадков 0%. Вечер — температура +1 °C, пасмурно, влажность 88%, давление 759 мм, ветер 3 м/с, вероятность осадков 0%. Ночь — температура +1 °C, пасмурно, влажность 88%, давление 758 мм, ветер 3 м/с, вероятность осадков 0%.

Переиспользуйте сниппеты в своих проектах.
Поставьте нам звездочку на GitHub!