Tool Calling @ Yandex Cloud with API, часть 2
Tool Calling, мотивация с точки зрения бизнеса
Интерактивные ноутбук и полезные утилиты, использованные в руководстве.
Код покрыт тестами и поддерживается.
Создаем ассистента для онлайн-маркетплейса
Продолжаем разработку нашего бота-ассистента, который будет помогать пользователю на онлайн-маркетплейсе. Пайплайн, по которому мы хотим пройти, как пользователь:
- Получить баланс пользователя
- Получить список товаров, которые мы можем купить, укладываясь в бюджет
- Оформить заказ
Давайте создадим несколько инструментов, которые нам пригодятся для нашего бота-ассистента.
- balance_tool - инструмент для получения баланса пользователя по его userId
- product_list_tool - инструмент для получения списка товаров с фильтрациями и сортировками
- order_tool - инструмент для создания заказа по productId
- balance_tool
- product_list_tool
- order_tool
balance_tool = {
"function": {
"name": "BalanceTool",
"description": "Получает баланс пользователя в рублях из базы данных.",
"parameters": {
"type": "object",
"properties": {
"userId": {
"type": "string",
"description": "Идентификатор пользователя, для которого нужно получить баланс."
}
},
"required": ["userId"],
"additionalProperties": False
}
}
}
product_list_tool = {
"function": {
"name": "ProductListTool",
"description": "Получает список товаров из базы данных на основе заданных параметров. Позволяет фильтровать товары по диапазону цен, категории и критерию сортировки.",
"parameters": {
"type": "object",
"properties": {
"priceMinMax": {
"type": "array",
"items": {
"type": "number"
},
"description": "Диапазон цен в формате [минимальная цена, максимальная цена]. Позволяет пользователю указать, какие товары его интересуют в пределах заданного ценового диапазона."
},
"category": {
"type": "string",
"description": "Категория товаров, по которой будет осуществляться фильтрация. Например, 'электроника', 'одежда', 'книги', 'дом и сад'. Это позволяет пользователю сузить поиск до определенной группы товаров."
},
"sortBy": {
"type": "string",
"enum": ["price", "popularity", "rating"],
"description": "Критерий сортировки товаров. Возможные значения: 'price' для сортировки по цене, 'popularity' для сортировки по популярности, 'rating' для сортировки по рейтингу. Это позволяет пользователю получить список товаров в удобном для него порядке."
}
},
"required": ["priceMinMax", "category"],
"additionalProperties": False
}
}
}
order_tool = {
"function": {
"name": "OrderTool",
"description": "инструмент, который заказывает товар по его идентификатору productId",
"parameters": {
"type": "object",
"properties": {
"productId": {
"type": "string",
"description": "Идентификатор товара, который нужно заказать."
},
"quantity": {
"type": "integer",
"description": "Количество товара для заказа.",
"default": 1,
"minimum": 1
}
},
"required": ["productId", "quantity"],
"additionalProperties": False
}
}
}
chat_completion_request(messages, tools=None) -> YandexGPTResponse
- функция для отправки запроса к YandexGPTget_function_call_from_response(response) -> toolCalls
- функция для получения объекта toolCalls из ответа YandexGPTprocess_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+ успешно оформлен.
Вызов функции с нестрогими параметрами
- Архитектура
- База данных
- process_sql_tool
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()
Теперь попробуем реализовать приложение 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"],
},
}
}