Simplifying the process of utilizing OpenAI models' function calling capabilities, a new approach involves creating a meticulously structured JSON list. Consider the following example:
{
"name": "get_current_weather",
"description": "Get the current weather",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"format": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The temperature unit to use. Infer this from the users location.",
},
},
"required": ["location", "format"],
},
}
Fortunately, manually performing this task for your Python functions is both tedious and time-consuming. Moreover, it leaves ample room for errors, making the process error-prone.
To simplify the process and provide a streamlined solution, we introduce (original work here: https://github.com/aurelio-labs/funkagent) a function converter that converts your Python functions to OpenAI API-compliant functions.
import functools
import inspect
import re
def type_mapping(dtype: type) -> str:
"""
Maps python types to OpenAPI types
:param dtype: Python type
:return:
"""
if dtype == float:
return "number"
elif dtype == int:
return "integer"
elif dtype == str:
return "string"
else:
return "string"
def extract_params(doc_str: str) -> dict[str, str]:
"""
Extracts parameters from docstring
:param doc_str: Docstring of a function or method
:return:
"""
# split doc string by newline, skipping empty lines
params_str = [line for line in doc_str.split("\n") if line.strip()]
params = {}
for line in params_str:
# we only look at lines starting with ':param'
if line.strip().startswith(':param'):
param_match = re.findall(r'(?<=:param )\w+', line)
if param_match:
param_name = param_match[0]
desc_match = line.replace(f":param {param_name}:", "").strip()
# if there is a description, store it
if desc_match:
params[param_name] = desc_match
return params
def func_to_json(func) -> dict[str, Any]:
"""
Converts a function to a json schema for OpenAI
:param func:
:return:
"""
# Check if the function is a functools.partial
if isinstance(func, functools.partial) or isinstance(func, functools.partialmethod):
fixed_args = func.keywords
_func = func.func
else:
fixed_args = {}
_func = func
# first we get function name
func_name = _func.__name__
# then we get the function annotations
argspec = inspect.getfullargspec(_func)
# get the function docstring
func_doc = inspect.getdoc(_func)
# parse the docstring to get the description
func_description = ''.join([line for line in func_doc.split("\n") if not line.strip().startswith(':')])
# parse the docstring to get the descriptions for each parameter in dict format
param_details = extract_params(func_doc) if func_doc else {}
# attach parameter types to params and exclude fixed args
# get params
params = {}
for param_name in argspec.args:
if param_name not in fixed_args.keys():
params[param_name] = {
"description": param_details.get(param_name) or "",
"type": type_mapping(argspec.annotations.get(param_name, type(None)))
}
# calculate required parameters excluding fixed args
# _required = [arg for arg in argspec.args if arg not in fixed_args]
_required = [i for i in argspec.args if i not in fixed_args.keys()]
if inspect.getfullargspec(_func).defaults:
_required = [argspec.args[i] for i, a in enumerate(argspec.args) if
argspec.args[i] not in inspect.getfullargspec(_func).defaults and argspec.args[
i] not in fixed_args.keys()]
# then return everything in dict
return {
"name": func_name,
"description": func_description,
"parameters": {
"type": "object",
"properties": params
},
"required": _required
}
With a few simple lines of code, you can harness the power of your existing Python code and imbue it with AI function calling.
Here are some functions to work with:
def func_with_no_params():
"""
This function has no parameters
:return:
"""
return 1
def func_with_mandatory_params_single_space_doc(a: str, b: str):
"""
This function has mandatory parameters
:param a:
:param b:
:return:
"""
return 1
def func_with_optional_params_single_space_doc(a: str, b: str = "b"):
"""
This function has optional parameters
:param a:
:param b:
:return:
"""
return 1
Below are some pytest examples.
def test_func_to_json_func_with_no_params():
"""
This function tests func_to_json with a function that has no parameters
:return:
"""
_json_fun = func_to_json(func_with_no_params)
assert _json_fun["name"] == "func_with_no_params"
assert _json_fun["description"] == "This function has no parameters"
assert 'properties' in _json_fun["parameters"]
assert 'type' in _json_fun["parameters"]
assert _json_fun["parameters"]["type"] == "object"
assert _json_fun["parameters"]["properties"] == {}
assert _json_fun["required"] == []
and its output:
{
"name": "func_with_no_params",
"description": "This function has no parameters",
"parameters": {
"type": "object",
"properties": {}
},
"required": []
}
def test_func_to_json_func_with_mandatory_params_single_space_doc():
"""
This function tests func_to_json with a function that has mandatory parameters and single space doc
:return:
"""
_json_fun = func_to_json(func_with_mandatory_params_single_space_doc)
print(json.dumps(_json_fun, indent=4))
assert _json_fun["name"] == "func_with_mandatory_params_single_space_doc"
assert _json_fun["description"] == "This function has mandatory parameters"
assert 'properties' in _json_fun["parameters"]
assert 'type' in _json_fun["parameters"]
assert _json_fun["parameters"]["type"] == "object"
assert _json_fun["parameters"]["properties"] == {
"a": {
"description": "",
"type": "string"
},
"b": {
"description": "",
"type": "string"
}
}
assert _json_fun["required"] == ["a", "b"]
and its output:
{
"name": "func_with_mandatory_params_single_space_doc",
"description": "This function has mandatory parameters",
"parameters": {
"type": "object",
"properties": {
"a": {
"description": "",
"type": "string"
},
"b": {
"description": "",
"type": "string"
}
}
},
"required": [
"a",
"b"
]
}
def test_func_to_json_partial_func_with_optional_params_single_space_doc():
"""
This function tests func_to_json with a function that has optional parameters and single space doc
:return:
"""
_json_fun = func_to_json(functools.partial(func_with_optional_params_single_space_doc, a="a"))
print(json.dumps(_json_fun, indent=4))
assert _json_fun["name"] == "func_with_optional_params_single_space_doc"
assert _json_fun["description"] == "This function has optional parameters"
assert 'properties' in _json_fun["parameters"]
assert 'type' in _json_fun["parameters"]
assert _json_fun["parameters"]["type"] == "object"
assert _json_fun["parameters"]["properties"] == {
"b": {
"description": "",
"type": "string"
}
}
assert _json_fun["required"] == []
and the output:
Note: the fixed args by the partial are not passed to the OpenAI function. This is really useful when you have locally bound vars such as states or some sort of DB session etc.
{
"name": "func_with_optional_params_single_space_doc",
"description": "This function has optional parameters",
"parameters": {
"type": "object",
"properties": {
"b": {
"description": "",
"type": "string"
}
}
},
"required": []
}