Tutorial: в gigachat появился function calling

от автора

UPD. 16 апреля 2024 Сбер открыл доступ к функционалу ещё для двух своих моделей, и теперь он доступен для GigaChat-preview, GigaChat-Plus-preview или GigaChat-Pro-preview. Запросы с функциями работают не очень стабильно, поэтому, если видите, что ответ долго не приходит, не стесняйтесь обновлять запрос.

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

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

Если вы попросите Chat GPT продать вам function calling, то получите примерно следующее:

Функция вызова в Chat GPT открывает новые возможности для разработчиков и предпринимателей, позволяя создавать более интеллектуальные и интерактивные приложения. С этой функцией вы можете:

Автоматизировать задачи: Используйте Chat GPT для автоматизации рутинных задач, таких как отправка электронных писем или обновление баз данных.
Интегрировать с внешними API: Подключайте вашего бота к внешним сервисам, чтобы получать актуальную информацию, например, о погоде или курсах валют.
Улучшать пользовательский опыт: Создавайте более глубокое взаимодействие с пользователями, предоставляя им полезные данные и услуги прямо в чате.

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

GigaChat же так хорошо function calling не продаёт, функционал этот ещё не доступен в gigachain и доступен через API только для модели "GigaChat-Pro-preview", поэтому я хочу поделиться своим первым опытом его использования.

На странице с документацией вас ожидает множество json схем, которые которые нужно добавить к запросам для вызова функций, а здесь мы рассмотрим, как внедрить вызов функций в свой код.

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

За основу я взял код из этого видео, рекомендую ознакомиться с ним для начала изучения работы с API GigaChat.

Начнём с авторизации.

from google.colab import userdata # auth token API GigaChat giga_key = userdata.get('GIGA_CHAT_KEY') # id гугл-таблицы из которой будем получать данные price_id = userdata.get('PRICE_ID')

С помощью auth token получим временный токен для обращения к API, токен действителен 30 минут:

import requests import uuid  def get_token(auth_token, scope='GIGACHAT_API_CORP'):     """       Выполняет POST-запрос к эндпоинту, который выдает токен.        Параметры:       - auth_token (str): токен авторизации, необходимый для запроса.       - область (str): область действия запроса API. По умолчанию — «GIGACHAT_API_PERS».        Возвращает:       - ответ API, где токен и срок его "годности".       """     # Создадим идентификатор UUID (36 знаков)     rq_uid = str(uuid.uuid4())      # API URL     url = "https://ngw.devices.sberbank.ru:9443/api/v2/oauth"      # Заголовки     headers = {         'Content-Type': 'application/x-www-form-urlencoded',         'Accept': 'application/json',         'RqUID': rq_uid,         'Authorization': f'Basic {auth_token}'     }      # Тело запроса     payload = {         'scope': scope     }      try:         # Делаем POST запрос с отключенной SSL верификацией         # (можно скачать сертификаты Минцифры, тогда отключать проверку не надо)         response = requests.post(url, headers=headers, data=payload, verify=False)         return response     except requests.RequestException as e:         print(f"Ошибка: {str(e)}")         return -1

Если пользуетесь GigaChat как физическое лицо, то укажите аргумент scope=«GIGACHAT_API_PERS»

response = get_token(giga_key) if response != 1:   print(response.text)   giga_token = response.json()['access_token']
{"access_token":"token","expires_at":1712650905952}

Запрос вернёт токен и срок его истечения в формате временной метки в миллисекундах, для наглядности преобразуем его в формат даты и времени:

from datetime import datetime  # Временная метка в миллисекундах timestamp_ms = response.json()["expires_at"]  # Преобразование в секунды timestamp_s = timestamp_ms / 1000  # Преобразование в объект datetime date_time = datetime.utcfromtimestamp(timestamp_s)  print(date_time.strftime('%Y-%m-%d %H:%M:%S'))
2024-04-09 08:21:45

Как я уже писал раннее, вызов функций доступен только для модели "GigaChat-Pro-preview", необходимо проверить что эта модель доступна на вашем тарифе:

import requests  url = "https://gigachat.devices.sberbank.ru/api/v1/models"  payload={} headers = {   'Accept': 'application/json',   'Authorization': f'Bearer {giga_token}' }  response = requests.request("GET", url, headers=headers, data=payload, verify=False)  print(response.text)
{"object":"list","data":[{"id":"GigaChat","object":"model","owned_by":"salutedevices"},{"id":"GigaChat-Plus","object":"model","owned_by":"salutedevices"},{"id":"GigaChat-Pro","object":"model","owned_by":"salutedevices"},{"id":"GigaChat-Pro-preview","object":"model","owned_by":"salutedevices"}]}

Если "GigaChat-Pro-preview" отображается в вашем списке, то можно переходить к следующему этапу.

Определим пользовательские функции:

def weather_forecast(location):   """   Получение погоду в Москве   """   return "в Москве +13"

Самая простая функция, которая будет выдавать фиксированную строку при любом обращении, но у неё на входе есть аргумент location, сейчас объясню для чего необходимо.

Функции передаются в модель в виде json схемы:

         {             "name": "weather_forecast",             "description": "Возвращает температуру на заданный период",             "parameters": {                 "type": "object",                 "properties": {                     "location": {                         "type": "string",                         "description": "Местоположение, например, название города"                     },                     "format": {                         "type": "string",                         "enum": [                             "celsius",                             "fahrenheit"                         ],                         "description": "Единицы изменерия температуры"                     },                     "num_days": {                         "type": "integer",                         "description": "Период, для которого нужно вернуть прогноз"                     }                 },                 "required": [                     "location",                     "num_days"                 ]             }         }

В этой схеме есть поле "properties", в котором указываются параметры, которые модель должна сгенерировать для этой функции, и "required", в котором указывается список обязательных аргументов. В случае, когда функция не требует аргументов на входе, в значениях этих полей по идее нужно указать пустой массив и пустой список, но иногда в таких случаях GigaChat начинает самостоятельно генерировать аргументы, которая функция не будет готова принимать, например для для функции с погодой у меня появился аргумент 'city':

{'choices': [{'message': {'content': '', 'role': 'assistant', 'function_call': {'name': 'weather_forecast', 'arguments': {'city': 'Москва'}}}, 'index': 0, 'finish_reason': 'function_call'}], 'created': 1712654212, 'model': 'GigaChat-Pro-preview:2.2.25.3', 'object': 'chat.completion', 'usage': {'prompt_tokens': 389, 'completion_tokens': 15, 'total_tokens': 404, 'system_tokens': 25}}

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

Определим ещё пару функций:

def get_prices(products):   if isinstance(products, str):     products = [products]   return json.dumps(price_list[price_list['вид продукции'].isin(products)][['вид продукции', 'цена за 1 кг, рубли РФ']].to_dict(orient='records'), ensure_ascii=False)

Для этой функции будет генерироваться список подходящих продуктов, для которых будут получены цены из прайс-листа.

def get_full_pricelist(products):     """     Получение информации по всей продукции     """     return json.dumps(price_list[['вид продукции', 'цена за 1 кг, рубли РФ']].to_dict(orient='records'), ensure_ascii=False)

Функция при запросе выдаёт все доступные цены из прайслиста, здесь так же используется фиктивный параметр 'products'. Создадим json схему функций для модели:

giga_functions = [     {         "name": "weather_forecast",         "description": "Возвращает температуру в Москве",         "parameters": {             "type": "object",             "properties": {                 "location": {                     "type": "string",                     "description": "Местоположение, например, название города"                     }             },             "required": ["location"]             },         "return_parameters": {             "type": "object",             "properties": {                 "location": {                     "type": "string",                     "description": "Местоположение, например, название города"                     },                 "temperature": {                     "type": "integer",                     "description": "Температура для заданного местоположения"                     },                 "forecast": {                     "type": "array",                     "items": {                         "type": "string"                         },                     "description": "Описание погодных условий"                     },                 "error": {                     "type": "string",                     "description": "Возвращается при возникновении ошибки. Содержит описание ошибки"                     }                 }             }         },         {             "name": "get_full_pricelist",             "description": "Возвращает цены на всю имеющуюуся продукцию",             "parameters": {                 "type": "object",                 "properties": {                     "products": {                         "type": "string",                         "description": f"список наиболее подходящих продуктов из списка {price_list['вид продукции'].to_list()}"                         }                     },                 "required": ["products"]                 },             "return_parameters": {                 "type": "object",                 "properties": {                     "product": {                         "type": "string",                         "description": "Наименование продукции"                         },                     "price": {                         "type": "integer",                         "description": "Цена продукции в рублях РФ"                         },                     "error": {                         "type": "string",                         "description": "Возвращается при возникновении ошибки. Содержит описание ошибки"                         }                     }                 }             },              {                  "name": "get_prices",                  "description": "Возвращает цену на запрашиваемый продукт",                  "parameters": {                      "type": "object",                      "properties": {                          "products": {                              "type": "string",                              "description": f"список наиболее подходящих продуктов из списка {price_list['вид продукции'].to_list()}"                              }                          },                      "required": [                          "products"                          ]                      },                  "few_shot_examples": [                      {                          "request": "сколько стоит лопатка?",                          "params": {                              "products": ["Свиная лопатка"]                              }                          }                      ],                  "return_parameters": {                      "type": "object",                      "properties": {                          "products": {                              "type": "string",                              "description": "Наименование продукции"                              },                          "price": {                              "type": "integer",                              "description": "Цена для данного вида продукции"                              },                          "error": {                              "type": "string",                              "description": "Возвращается при возникновении ошибки. Содержит описание ошибки"                              }                          }                      }                  }     ]

Здесь описанием аргумента функции ''get_prices является f-строка со списком из продуктов, которые есть в прайслисте, чтобы модель генерировала корректные аргументы для обращения к функции, это почти всегда работает, но исключения конечно бывают.

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

import requests import json  def get_chat_completion(auth_token, user_message, conversation_history=None):     """     Отправляет POST-запрос к API чата для получения ответа от модели GigaChat в рамках диалога.      Параметры:     - auth_token (str): Токен для авторизации в API.     - user_message (str): Сообщение от пользователя, для которого нужно получить ответ.     - conversation_history (list): История диалога в виде списка сообщений (опционально).      Возвращает:     - response (requests.Response): Ответ от API.     - conversation_history (list): Обновленная история диалога.     """     # URL API, к которому мы обращаемся     url = "https://gigachat.devices.sberbank.ru/api/v1/chat/completions"      # Если история диалога не предоставлена, добавляем в неё системный промт     if conversation_history is None or conversation_history == []:         conversation_history = [             {                 "role": "system",                 "content": "ты менеджер по продажам, твоя задача сообщать цены из прайс-листа и погоду"                 }             ]      # Добавляем сообщение пользователя в историю диалога     conversation_history.append({         "role": "user",         "content": user_message     })     # json схемы доступных функций     giga_functions = [     {         "name": "weather_forecast",         "description": "Возвращает температуру в Москве",         "parameters": {             "type": "object",             "properties": {                 "location": {                     "type": "string",                     "description": "Местоположение, например, название города"                     }             },             "required": ["location"]             },         "return_parameters": {             "type": "object",             "properties": {                 "location": {                     "type": "string",                     "description": "Местоположение, например, название города"                     },                 "temperature": {                     "type": "integer",                     "description": "Температура для заданного местоположения"                     },                 "forecast": {                     "type": "array",                     "items": {                         "type": "string"                         },                     "description": "Описание погодных условий"                     },                 "error": {                     "type": "string",                     "description": "Возвращается при возникновении ошибки. Содержит описание ошибки"                     }                 }             }         },         {             "name": "get_full_pricelist",             "description": "Возвращает цены на всю имеющуюуся продукцию",             "parameters": {                 "type": "object",                 "properties": {                     "products": {                         "type": "string",                         "description": f"список наиболее подходящих продуктов из списка {price_list['вид продукции'].to_list()}"                         }                     },                 "required": ["products"]                 },             "return_parameters": {                 "type": "object",                 "properties": {                     "product": {                         "type": "string",                         "description": "Наименование продукции"                         },                     "price": {                         "type": "integer",                         "description": "Цена продукции в рублях РФ"                         },                     "error": {                         "type": "string",                         "description": "Возвращается при возникновении ошибки. Содержит описание ошибки"                         }                     }                 }             },              {                  "name": "get_prices",                  "description": "Возвращает цену на запрашиваемый продукт",                  "parameters": {                      "type": "object",                      "properties": {                          "products": {                              "type": "string",                              "description": f"список наиболее подходящих продуктов из списка {price_list['вид продукции'].to_list()}"                              }                          },                      "required": [                          "products"                          ]                      },                  "few_shot_examples": [                      {                          "request": "сколько стоит лопатка?",                          "params": {                              "products": ["Свиная лопатка"]                              }                          }                      ],                  "return_parameters": {                      "type": "object",                      "properties": {                          "products": {                              "type": "string",                              "description": "Наименование продукции"                              },                          "price": {                              "type": "integer",                              "description": "Цена для данного вида продукции"                              },                          "error": {                              "type": "string",                              "description": "Возвращается при возникновении ошибки. Содержит описание ошибки"                              }                          }                      }                  }     ]     # Подготовка данных запроса в формате JSON     payload = json.dumps({         "model": "GigaChat-Pro-preview",         "messages": conversation_history,         "function_call": "auto",         "functions": giga_functions,         "temperature": 0.5,         "top_p": 0.1,         "n": 1,         "stream": False,         "max_tokens": 32000,         "repetition_penalty": 1,         "update_interval": 0     })     # Заголовки запроса     headers = {         'Content-Type': 'application/json',         'Accept': 'application/json',         'Authorization': f'Bearer {auth_token}'     }     # словарь с функциями для обработки запроса     available_functions = {         "weather_forecast": weather_forecast,         "get_full_pricelist": get_full_pricelist,         "get_prices": get_prices         }     # Выполнение POST-запроса и возвращение ответа     try:         response = requests.post(url, headers=headers, data=payload, verify=False)         response_data = response.json()         # проверяем ответ модели на наличие обращений к функциям         try:           # json с информацией о необходимой функции           func_calls = response_data['choices'][0]['message']['function_call']           # имя вызываемой функции           func_name = func_calls['name']           # аргументы вызываемой функции           func_args = func_calls['arguments']           # достаём нужную функцию из словаря            function_to_call = available_functions[func_name]                      # добавляем в историю сообщений ответ модели с вызовом функции, БЕЗ ЭТОГО МОДЕЛЬ НЕ ОТВЕТИТ           conversation_history.append(response_data['choices'][0]['message'])           # добавляем в историю сообщений результаты функции           conversation_history.append(               {                   "role": "function",                   "content": function_to_call(**func_args),                   "name": func_name                   }               )           # обновляем данные           payload = json.dumps({               "model": "GigaChat-Pro-preview",               "messages": conversation_history,               "function_call": "auto",               "functions": giga_functions,               "temperature": 0.5,               "top_p": 0.1,               "n": 1,               "stream": False,               "max_tokens": 32000,               "repetition_penalty": 0.5,               "update_interval": 0               })           # повторяем зарос             response = requests.post(url, headers=headers, data=payload, verify=False)           response_data = response.json()           # for func in func_calls:         except:           pass          # Добавляем ответ модели в историю диалога         conversation_history.append({             "role": "assistant",             "content": response_data['choices'][0]['message']['content']         })          return response, conversation_history     except requests.RequestException as e:         # Обработка исключения в случае ошибки запроса         print(f"Произошла ошибка: {str(e)}")         return None, conversation_history

Примеры получаемых ответов:

conversation_history = []  response, conversation_history = get_chat_completion(giga_token, "какая погода в Москве?", conversation_history)
[{'role': 'system',   'content': 'ты менеджер по продажам, твоя задача сообщать цены из прайс-листа и погоду'},  {'role': 'user', 'content': 'какая погода в Москве?'},  {'content': '',   'role': 'assistant',   'function_call': {'name': 'weather_forecast',    'arguments': {'location': 'Москва'}}},  {'role': 'function', 'content': 'в Москве +13', 'name': 'weather_forecast'},  {'role': 'assistant', 'content': 'В Москве сейчас +13°С.'}]

узнали о погоде

conversation_history = []  response, conversation_history = get_chat_completion(giga_token, "дай цены по всей продукции", conversation_history)
[{'role': 'system',   'content': 'ты менеджер по продажам, твоя задача сообщать цены из прайс-листа и погоду'},  {'role': 'user', 'content': 'дай цены по всей продукции'},  {'content': '',   'role': 'assistant',   'function_call': {'name': 'get_full_pricelist',    'arguments': {'products': 'всю'}}},  {'role': 'function',   'content': '[{"вид продукции": "Тушка индейки самец", "цена за 1 кг, рубли РФ": "295"}, ... , {"вид продукции": "Свиная шея", "цена за 1 кг, рубли РФ": "400"}]',   'name': 'get_full_pricelist'},  {'role': 'assistant',   'content': 'Цены на всю продукцию следующие: Тушка индейки самец - 295 рублей за 1 кг, ... , Свиная шея - 400 рублей за 1 кг.'}]

получили цены из прайслиста

conversation_history = []  response, conversation_history = get_chat_completion(giga_token, "сколько стоит рагу и какая погода в москве?", conversation_history)
[{'role': 'system',   'content': 'ты менеджер по продажам, твоя задача сообщать цены из прайс-листа и погоду'},  {'role': 'user', 'content': 'сколько стоит рагу и какая погода в москве?'},  {'content': '',   'role': 'assistant',   'function_call': {'name': 'get_prices',    'arguments': {'products': 'Рагу индейки  (монолит), Рагу индейки (лоток)'}}},  {'role': 'function', 'content': '[]', 'name': 'get_prices'},  {'role': 'assistant',   'content': 'Извините, но я не могу предоставить вам информацию о цене рагу. Могу ли я помочь вам с чем-то ещё?'}]

Здесь модель сгенерировала аргумент строкой, а не списком, поэтому продукты не нашлись в датафрейме, этот случай можно обработать. Другое дело, что здесь модель вызывает только одну функцию, а не две, как требует контекст. Впрочем не надо забывать, что функционал находится на этапе 'preview' и это скорее всего улучшат.

Вот такой мой первый опыт использования вызова функций в API GigaChat, делитесь своими идеями/мнениями/советами на данную тему, весь код выложу на гитхаб и ещё раз поделюсь видео, с которого я начал своё знакомство c API GigaChat.

Автор статьи: Сергей Романов


ссылка на оригинал статьи https://habr.com/ru/articles/806627/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *