OpenAI Function Calling і як з ним працювати в Python-проєктах

Усі статті, обговорення, новини про AI — в одному місці. Підписуйтеся на DOU | AI!

Вітаю, спільното! Мене звуть Марк, я Back-end Engineer в компанії Welltech.

Останній рік у сфері технологій безумовно можна назвати роком штучного інтелекту. Тож стежимо за новими гравцями та фічами від OpenAI. Нещодавно компанія додала доволі цікаву штуку під назвою Function Calling.

Що таке Function Calling в OpenAI? Двома словами: виконуючи запити до OpenAI chat completion API, ви можете описати одну або кілька функцій та їхні параметри. Потім OpenAI визначить найбільш придатну функцію для використання (якщо її взагалі слід використовувати), а також параметри для передачі функції на основі вашого повідомлення.

Одразу треба зазначити важливий момент: OpenAI НЕ викликає ваші функцій. Натомість він визначає функцію та параметри, які слід передати у вашу функцію. Виконання функції — це все ще відповідальність вашого коду.

Оʼкей, перейдемо до прикладів.

Наприклад, у вас є така функція на Python:


def get_weather(location: str, unit: Literal["c", "f"] = None):
    ...

І ось так в OpenAI completion API документації виглядає приклад опису цієї функції під час API-запиту:


{
  "name": "get_weather",
  "description": "Determine weather in my location",
  "parameters": {
    "type": "object",
    "properties": {
      "location": {
        "type": "string"
      },
      "unit": {
        "type": "string",
        "enum": [
          "c",
          "f"
        ]
      }
    },
    "required": [
      "location"
    ]
  }
}

Це опис функції в JSON Schema. Хоча формат опису виглядає зрозумілим і навіть очевидним, але в нього є помітний недолік — цей формат зовсім не лаконічний. Також під час опису функцій в такому форматі «вручну» ви порушуєте DRY принцип, бо ваша функція описана двічі — один раз в коді та другий раз в форматі JSON Schema.

Саме тому мені захотілось автоматизувати цей процес.

Чи існує для цього готові рішення? Так, на просторах GitHub я знайшов такий проєкт:
В цьому репозиторії генерація JSON Schema з функції робиться за допомогою модуля inspect та бібліотеки docstring-parser. Мені здалось що це «занадто», тому я вирішив зробити свій, більш простий хелпер для цієї задачі.

Перше, що прийшло на думку, — це додавати детальну документацію функції і використати docutils для того, щоб розпарсити документацію. Але коли я зробив чернетку коду для генерації схеми, то зрозумів, що таке рішення теж потребує забагато коду для такої начебто простої задачі.

Другою ідеєю було використання Pydantic, оскільки ним часто користуються для автогенерації схеми з коду. З таким підходом єдине, що нам потрібно, — це описати функцію як Pydantic-модель. Після цього генерація схеми функції з класу стає зовсім простою задачею. Ось як може виглядати наш базовий клас для опису функції:


from pydantic import BaseMode

class FunctionDefinition(BaseModel):

    @classmethod
    def to_function_definition(cls):
        return {
            "name": cls.__name__,
            "description": cls.__doc__,
            "parameters": cls.schema(),
      }
1. Ви можете піти далі та зробити декоратор, який би генерував відповідний клас з функції, але, на мою думку, це зайве ускладнення.
2. Для більш просунутих AI користовачів для аналогічного функціоналу можна використати Pydantic Parser в LangChain.

Розберемо використання цього класу FunctionDefinition на прикладі простої функції для підрахунку заробітної плати робітника компанії:


def calculate_salary(
      hourly_rate: int,
      regular_hours: int,
      overtime_hours: int = 0,
      bonus: int = 0):
    """Makes salary calculations"""
  ...

Так буде виглядати опис «функції» у вигляді класу:


class CalculateSalaryFirstDepartment(FunctionDefinition):   
    """Makes calculations of salary for first department"""
    hourly_rate: int
    regular_hours: int
    overtime_hours: int = 0
    bonus: int = 0

Тепер додамо саму функцію обчислення та метод для виводу підрахунку:


class CalculateSalaryFirstDepartment(FunctionDefinition):   
  ...
  # так в Pydantic ми описуємо атрибут класа, який не має бути частиною схеми
  _overtime_rate_multiplier: float = PrivateAttr(default=1.5)
  def process(self) -> int:
      return int(
          self.hourly_rate * self.regular_hours
          + self._overtime_rate_multiplier * self.hourly_rate * self.overtime_hours
          + self.bonus
      )
  def calculation_description(self):
      return (
          f"Calculation: (hourly_rate * regular_hours) + "
          f"(overtime_rate_multiplier * hourly_rate * overtime_hours) + bonus\n"
          f"({self.hourly_rate} * {self.regular_hours}) + "
          f"({self._overtime_rate_multiplier} * {self.hourly_rate} *    {self.overtime_hours}) + {self.bonus}"
  )

Для того, щоб трохи наблизити наш приклад до реальних кейсів, уявімо, що формули відрізняються залежно від відділу компанії, й у нас є декілька функцій, по одній на кожен відділ.

Тоді ось так виглядатиме клас-функція для розрахунків для другого відділу:


class CalculateSalarySecondDepartment(CalculateSalaryFirstDepartment):
    """Makes calculations of salary for second department"""
  _overtime_rate_multiplier: float = PrivateAttr(default=2)

Тепер можемо зібрати до купи наші класи-калькулятори з викликом OpenAI API.

У наступному прикладі наведена функція, яка за допомого GPT формує аргументи для одного з класів-калькуляторів на основі текстового повідомлення.


def calculate_salary(user_input: str) -> int:
    """
    Calculates daily salary from user input text
    """
    
    system_msg = (
      "You are a helper to calculate daily employee salary. "
      "Parse user input and extract arguments for the function call. "
      "IMPORTANT: If user works more than 8 hours per day, this time is considered as "
      "overtime_hours. "
    )
    response = openai.ChatCompletion.create(
      model="gpt-3.5-turbo-0613",
      messages=[
          {"role": "system", "content": system_msg},
          {"role": "user", "content": user_input},
      ],
      functions=[
          CalculateSalaryFirstDepartment.to_function_definition(),
          CalculateSalarySecondDepartment.to_function_definition(),
      ],
    )
  choice = response["choices"][0]

  # If there is not enough data, chat can ask for clarification, 
  # so while loop can be required in case of real-time interaction with users
  if not choice["message"].get("function_call"):
      return choice["message"]["content"]

  function_call = choice["message"]["function_call"]

  # select class by function name
  calculator_class = globals()[function_call["name"]]
  function_arguments = json.loads(function_call["arguments"])

  # create class instance with arguments
  calculator = calculator_class(**function_arguments)

  print(calculator.calculation_description())

  return calculator.process()

Нижче наведено приклад виклику цієї функції:


calculate_salary("First department. Today I worked 10 hours. My rate - $20, bonus $100")
Calculation: (hourly_rate * regular_hours) + (overtime_rate_multiplier * hourly_rate * overtime_hours) + bonus
(20 * 8) + (1.5 * 20 * 2) + 100
Out[10]: 320

Треба зазначити, що в нашому прикладі формула розрахунків доволі проста для того, щоб ChatGPT міг сам нарахувати заробітну плату під час опису формули «словами» в system message. У реальних проєктах ваші функції скоріше за все будуть робити запити до інших сервісів або виконувати SQL-запити тощо. Задачею ж GPT залишається підготовка аргументів виклику функцій з блоку текста, а також вибір самої функції, якщо описано декілька функцій.

Зверніть увагу, що functions працюють в моделях gpt-4-0613 and gpt-3.5-turbo-0613. Тому якщо у вашому проєкті вже використовується OpenAI completion, може знадобитись оновлення GPT-моделі.

У разі, якщо ваші функції складніші, то скоріше за все ChatGPT потребує підказок у вигляді опису аргументів. Тоді в Pydantic це можна зробити так:


from pydantic import Field 

class MySuperFunction(FunctionDefinition):
  some_complex_argument: int = Field(description="Full description goes here")

На цьому на сьогодні все. Ця проста ідея та реалізація позбавить вас необхідності вручну описувати JSON Schema для функцій при використанні Function Calling. Сподіваюсь, що комусь буде в пригоді.

Повний приклад коду з цієї статті можна знайти тут.

👍ПодобаєтьсяСподобалось7
До обраногоВ обраному2
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Рішення автора не здається лаконічним). адаптація до будь-яких інших функцій не виглядає зручною, але це не суть, кльово що такий PoC працює. Будь-ласка, поправте лінк на гітхаб репозиторій з проектом, на який ви посилаєтесь (наразі 404)

Вітаю:)
суть рішення зводиться до класу FunctionDefinition, коротче вже немає куди:)

Весь наступний код є прикладом використання цього класу і function calling загалом.

Можете пояснити як саме і коли ті функції викликаються? Ви написалі функцію на пайтоні і вона якось завантажується на сервери openai ? тобто сам код на пайтоні ?
Чи ці виклики відбуваються якось локально?

Тут назва «function calling» трохи збиває з пантелику. Виклик функцій відбувається локально, тобто це робите ви так як вам зручно. А задача на стороні OpenAI це підготовка аргументів для виклику функції.

Підписатись на коментарі