Skip to main content

Overview

The hidden power behind chat.register(ƒ) is the Function Registry. It lets you register typed functions quickly and easily so they can be called on behalf of chat models.

[1]:
from chatlab import FunctionRegistry

def f(x: float):
'''Multiply x by 2.'''
return x*2

registry = FunctionRegistry()
registry.register(f)

await registry.call("f", "{\"x\": 4}")
[1]:
8

Functions can be registered by:

  • Passing a typed function in directly
  • Passing the function and the Pydantic model that describes its parameters
  • Passing the function and a JSON schema that describes its parameters

Registering Typed Functions

tip

Not all function parameters can be accurately determined. For those cases it's best to use a Pydantic model or JSON schema.

If you're adventurous, make a PR to chatlab to improve the type inference!

No Parameters

[2]:
import random

def flip_a_coin():
'''Flip a coin.'''
return random.choice(["heads", "tails"])

registry.register(flip_a_coin)
[2]:
{
'name': 'flip_a_coin',
'description': 'Flip a coin.',
'parameters': {
'type': 'object',
'properties': {},
'required': []
}
}

Multiple Parameters

[3]:
def add(x: float, y: float):
'''Add x and y.'''
return x + y

registry.register(add)
[3]:
{
'name': 'add',
'description': 'Add x and y.',
'parameters': {
'type': 'object',
'properties': {
'x': {'type': 'number'},
'y': {'type': 'number'}
},
'required': ['x', 'y']
}
}

Optional Parameters

[4]:
def add(x: float, y: float = 1):
'''Add x and y.'''
return x + y

registry.register(add)
[4]:
{
'name': 'add',
'description': 'Add x and y.',
'parameters': {
'type': 'object',
'properties': {
'x': {'type': 'number'},
'y': {'type': 'number'}
},
'required': ['x']
}
}

Registering Functions with Pydantic Models

Pydantic Models give you all the benefits above and much more:

  • Default values
  • Descriptions on parameters
  • Deeper type validation
[5]:
from chatlab import FunctionRegistry
from pydantic import BaseModel

registry = FunctionRegistry()


class CompoundInterestModel(BaseModel):
principal: float
rate: float
times_compounded: int
years: float


@registry.register(parameter_schema=CompoundInterestModel)
def compound_interest(principal: float, rate: float, times_compounded: int, years: float) -> float:
"""
Calculates the future value of an investment using compound interest.
:param principal: Initial investment amount
:param rate: Annual interest rate (as a decimal)
:param times_compounded: Number of times interest is compounded per year
:param years: Number of years the money is invested
:return: Future value of the investment
"""
return principal * (1 + rate / times_compounded) ** (times_compounded * years)

await registry.call(
"compound_interest",
CompoundInterestModel(principal=1000, rate=0.05, times_compounded=12, years=5).json()
)
[5]:
1283.3586785035118;

Additional Descriptions

Using Field, you can add descriptions to your parameters.

[6]:
from pydantic import BaseModel, Field

class Add(BaseModel):
x: float = Field(..., description="The first number to add.")
y: float = Field(1, description="The second number to add.")

def add(x: float, y: float = 1):
'''Add x and y.'''
return x + y

registry.register(add, Add)
[6]:
{
'name': 'add',
'description': 'Add x and y.',
'parameters': {
'title': 'Add',
'type': 'object',
'properties': {
'x': {
'title': 'X',
'description': 'The first number to add.',
'type': 'number'
},
'y': {
'title': 'Y',
'default': 1,
'description': 'The second number to add.',
'type': 'number'
}
},
'required': ['x']
}
}

Registering Functions with JSON Schemas

If you don't want to use Pydantic, you can write pure JSON schemas to describe your functions.

[7]:
def add(x: float, y: float = 1):
'''Add x and y.'''
return x + y

registry.register(add, {
"title": "Add",
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number", "default": 1}
},
"required": ["x"]
})

Direct OpenAI API Calls with the Function Registry

When calling OpenAI's Chat Completion endpoint you can tell it what kind of functions you have directly:

[8]:
import openai

openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=[
{"role": "user", "content": "Multiply 1024 by 2"},
],
functions=[
{
'name': 'f',
'description': 'Multiply x by 2.',
'parameters': {
'type': 'object',
'properties': {'x': {'type': 'number'}},
'required': ['x']
}
}
],
function_call="auto"
)

Function registries expose an api_manifest() method that returns a dictionary for passing directly to OpenAI's Chat Completion endpoint. This is useful if you have a lot of functions to register.

[9]:
import random

def secret():
'''Returns a secret'''
return random.randint(1, 10)

registry = FunctionRegistry()
registry.register(secret)

registry.api_manifest()
[9]:
{
'functions': [
{
'name': 'secret',
'description': 'Returns a secret',
'parameters': {
'type': 'object',
'properties': {},
'required': []
}
}
],
'function_call': 'auto'
}

This can be passed directly to ChatCompletion.create():

[10]:
resp = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=[
{"role": "user", "content": "Multiply 1024 by 2"},
],
**registry.api_manifest(),
).choices[0]

resp
[10]:
<OpenAIObject at 0x1198aef90> JSON: {
"finish_reason": "function_call",
"index": 0,
"message": {
"content": null,
"function_call": {
"arguments": "{}",
"name": "secret"
},
"role": "assistant"
}
}

From there, the response's message.function_call can be passed to registry.call() to execute the function:

[11]:
value = await registry.call(**resp.message.function_call)
value
[11]:
Assistant 10