Function Calling#
Sometimes called “tool usage”, function calling gives language models the ability to choose when to call a function you provide based off its documentation.
With kani, you can write functions in Python and expose them to the model with just one line of code: the
@ai_function
decorator.
Step 1: Subclass Kani#
To create a kani with function calling, make a subclass of Kani
and write your functions as methods.
For example, you might call an API, perform some math, or retrieve information from the internet - the possibilities are limitless.
from kani import Kani, chat_in_terminal
from kani.engines.openai import OpenAIEngine
api_key = "sk-..."
engine = OpenAIEngine(api_key, model="gpt-3.5-turbo")
class MyKani(Kani):
# step 1: write your methods
def get_weather(self, location, unit):
# call some weather API...
ai = MyKani(engine)
chat_in_terminal(ai)
Note
AI functions can be synchronous (i.e. def
) or asynchronous (async def
) - kani will automatically await a
coroutine as needed.
Step 2: Documentation#
In order for a language model to effectively know what our AI functions do, we need to document them. We do this inline in the function: through type annotations and the docstring.
The allowed types are:
an enum (
enum.Enum
)a list or dict of the above types (e.g.
list[str]
,dict[str, int]
,list[SomeEnum]
)
When the AI calls into the function, kani validates the AI’s requested parameters and guarantees that the passed parameters are of the annotated type by the time they reach your code.
Name & Descriptions#
By default, the function’s description will be taken from its docstring, and name from the source.
To specify the descriptions of parameters, you can provide an AIParam
annotation using a
typing.Annotated
type annotation.
For example, you might annotate a parameter timezone: str
with an example, like
timezone: Annotated[str, AIParam("The IANA time zone, e.g. America/New_York")]
.
Example#
Now, let’s put this all together: let’s tell the language model what we expect in the location, and that the unit should be either fahrenheit or celsius.
import enum
from typing import Annotated
# don't forget to import AIParam!
from kani import AIParam, Kani, chat_in_terminal
# ...
# for a param with limited choices, define an enum:
class Unit(enum.Enum):
FAHRENHEIT = "fahrenheit"
CELSIUS = "celsius"
class MyKani(Kani):
def get_weather(
self,
# give the model more information about a parameter by annotating it with AIParam:
location: Annotated[str, AIParam(desc="The city and state, e.g. San Francisco, CA")],
# or it can determine which of a limited set of options to use from an enum:
unit: Unit,
):
# add a triple-quoted string immediately after the def to describe the function:
"""Get the current weather in a given location."""
# call some weather API...
# ...
Note
Comments (i.e. # ...
) aren’t given to the language model at all - these are only for your own reference.
Step 3: Register#
The final step once you’ve defined your method is to register it as an AI function using the @ai_function()
decorator.
Here, you can set some options for how kani should expose your function by passing these keyword args:
- kani.ai_function(
- func=None,
- *,
- after: ChatRole = ChatRole.ASSISTANT,
- name: str | None = None,
- desc: str | None = None,
- auto_retry: bool = True,
- json_schema: dict | None = None,
- auto_truncate: int | None = None,
Decorator to mark a method of a Kani to expose to the AI.
- Parameters:
after – Who should speak next after the function call completes (see Next Actor). Defaults to the model.
name – The name of the function (defaults to the name of the function in source code).
desc – The function’s description (defaults to the function’s docstring).
auto_retry – Whether the model should retry calling the function if it gets it wrong (see Retry & Model Feedback).
json_schema – A JSON Schema document describing the function’s parameters. By default, kani will automatically generate one, but this can be helpful for overriding it in any tricky cases.
auto_truncate – If a function response is longer than this many tokens, truncate it until it is at most this many tokens and add “…” to the end. By default, no responses will be truncated. This uses a smart paragraph-aware truncation algorithm.
# don't forget to import ai_function!
from kani import AIParam, Kani, ai_function, chat_in_terminal
# ...
class MyKani(Kani):
@ai_function()
def get_weather(
self,
location: Annotated[str, AIParam(desc="The city and state, e.g. San Francisco, CA")],
unit: Unit,
):
"""Get the current weather in a given location."""
# call some weather API...
# ...
See also
The ai_function()
API reference.
Next Actor#
After a function call returns, kani will hand control back to the LM to generate a response by default. If instead
control should be given to the human (i.e. return from the chat round), set after=ChatRole.USER
.
Note
If the model calls multiple tools in parallel, the model will be allowed to generate a response if any function
has after=ChatRole.ASSISTANT
(the default) once all function calls are complete.
Complete Example#
Here’s the full example of how you might implement a function to get weather that we built in the last few steps:
import enum
from typing import Annotated
from kani import AIParam, Kani, ai_function, chat_in_terminal
from kani.engines.openai import OpenAIEngine
api_key = "sk-..."
engine = OpenAIEngine(api_key, model="gpt-3.5-turbo")
class Unit(enum.Enum):
FAHRENHEIT = "fahrenheit"
CELSIUS = "celsius"
class MyKani(Kani):
@ai_function()
def get_weather(
self,
location: Annotated[str, AIParam(desc="The city and state, e.g. San Francisco, CA")],
unit: Unit,
):
"""Get the current weather in a given location."""
# call some weather API, or just mock it for this example
degrees = 72 if unit == Unit.FAHRENHEIT else 22
return f"Weather in {location}: Sunny, {degrees} degrees {unit.value}."
ai = MyKani(engine)
chat_in_terminal(ai)
Few-Shot Prompting#
Just as in the last section, we can also few-shot prompt the model to give it examples of how it should call the functions we define.
When a function returns a result, that result is converted to a string and saved to the chat history. To few-shot
prompt a model, we can mock these returns in the chat history using ChatMessage.function()
!
For example, here’s how you might prompt the model to give the temperature in both Fahrenheit and Celsius without the user having to ask:
# build the chat history with examples
fewshot = [
ChatMessage.user("What's the weather in Philadelphia?"),
ChatMessage.assistant(
content=None,
# use a walrus operator to save a reference to the tool call here...
tool_calls=[
tc := ToolCall.from_function("get_weather", location="Philadelphia, PA", unit="fahrenheit")
],
),
ChatMessage.function(
"get_weather",
"Weather in Philadelphia, PA: Partly cloudy, 85 degrees fahrenheit.",
# ...so this function result knows which call it's responding to
tc.id
),
# and repeat for the other unit
ChatMessage.assistant(
content=None,
tool_calls=[
tc2 := ToolCall.from_function("get_weather", location="Philadelphia, PA", unit="celsius")
],
),
ChatMessage.function(
"get_weather",
"Weather in Philadelphia, PA: Partly cloudy, 29 degrees celsius.",
tc2.id
),
ChatMessage.assistant("It's currently 85F (29C) and partly cloudy in Philadelphia."),
]
# and give it to the kani when you initialize it
ai = MyKani(engine, chat_history=fewshot)
from kani import ChatMessage, FunctionCall
fewshot = [
ChatMessage.user("What's the weather in Philadelphia?"),
# first, the model should ask for the weather in fahrenheit
ChatMessage.assistant(
content=None,
function_call=FunctionCall.with_args(
"get_weather", location="Philadelphia, PA", unit="fahrenheit"
)
),
# and we mock the function's response to the model
ChatMessage.function(
"get_weather",
"Weather in Philadelphia, PA: Partly cloudy, 85 degrees fahrenheit.",
),
# repeat in celsius
ChatMessage.assistant(
content=None,
function_call=FunctionCall.with_args(
"get_weather", location="Philadelphia, PA", unit="celsius"
)
),
ChatMessage.function(
"get_weather",
"Weather in Philadelphia, PA: Partly cloudy, 29 degrees celsius.",
),
# finally, give the result to the user
ChatMessage.assistant("It's currently 85F (29C) and partly cloudy in Philadelphia."),
]
ai = MyKani(engine, chat_history=fewshot)
>>> chat_in_terminal(ai)
USER: What's the weather in San Francisco?
AI: Thinking (get_weather)...
AI: Thinking (get_weather)...
AI: It's currently 72F (22C) and sunny in San Francisco.
Few-shot prompts combined with function calls are a powerful tool! For example, you can also specify how a model should retry functions, vary the parameters it gives, react to function feedback, and more.
Dynamic Functions#
Rather than statically defining the list of functions a kani can use in a class, you can also pass a list of
AIFunction
when you initialize a kani.
The API for the AIFunction
class is similar to ai_function()
.
def my_cool_function(
foo: str,
bar: Annotated[int, AIParam(desc="Some cool parameter.")],
):
"""Do some cool things."""
...
functions = [AIFunction(my_cool_function)]
ai = Kani(engine, functions=functions)
Retry & Model Feedback#
If the model makes an error when attempting to call a function (e.g. calling a function that does not exist or passing params with invalid, non-coercible types) or the function raises an exception, Kani will send the error in a message to the model by default, allowing it up to retry_attempts to correct itself and retry the call.
Note
If the model calls multiple tools in parallel, the model will be allowed a retry if any exception handler allows it. This will only count as 1 retry attempt regardless of the number of functions that raised an exception.
In the next section, we’ll discuss how to customize this behaviour, along with other parts of the kani interface.
Internal Representation#
Changed in version v0.6.0.
As of Nov 6, 2023, OpenAI added the ability for a single assistant message to request calling multiple functions in
parallel, and wrapped all function calls in a ToolCall
wrapper. In order to add support for this in kani while
maintaining backwards compatibility with OSS function calling models, a ChatMessage
actually maintains the
following internal representation:
ChatMessage.function_call
is actually an alias for ChatMessage.tool_calls[0].function
. If there is more
than one tool call in the message, kani will raise an exception.
A ToolCall is effectively a named wrapper around a FunctionCall
, associating the request with a generated
ID so that its response can be linked to the request in future rounds of prompting.