openai-func-enums:
openai-func-enums is an unofficial Rust library for OpenAI. It contains a set of procedural macros and other functions, to be used in conjunction with async-openai, that make it easy to use enums to compose "function" tool types that can be passed to OpenAI's chat completions api.
If you want to see a larger example where there are more function definitions than there is context window, check out dripgrep.
Why?
The motivation for this was the need to leverage OpenAI function calls for logic control flow. If you have a lot of "function calls" to deal with, especially if they share argument types, the out-of-the-box way of doing this is unwieldy with using just async-openai. This library allows returns to be deserialized as instances of structs, the types of which the macros produce, so that you can easily take the response and match on the variants selected by the model.
Features
-
Enums are the greatest: openai-func-enums asks you to define an enum to represent possible "functions" to be passed to the OpenAI API, with each variant representing a function, with the fields on these variants indicating the required arguments. Each field can be either an enum, a value type, or a vector of value types, with the variants of the enum fields determining the allowed choices that can be passed to the OpenAI API.
-
Token Tallying: The library keeps a tally of the token count associated with each "function" defined through the enums.
-
Embedding-Based Function Filtering: Building your application with the feature
--compile_embeddings_all
will get embeddings for your functions and bake them into a zero-copy archive available at runtime. A per request token budget for tool definitions will be used to limit tools to what is most similar to the prompt. You can also specify functions that must be included no matter their similarity rank. A feature flag for updating changes only is there, but doesn't work yet because it is more involved. It uses rkyv(zero-copy deserialization framework) for serialization/deserialization. -
clap-gpt: This library provides macros and traits to allow you to turn an existing clap application into a clap-gpt application without a ton of extra ceremony required. See the usage section for an example.
-
Parallel tool calls: If OpenAI elects to call more than one of the available tools at the same time, this library will process them based on an execution strategy you specify. It can run them asynchronously, synchronously, or on os threads depending on your need. The clap integration example goes into more detail about parallel tool calls.
Usage
**Note: This library requires async-openai, which requires that you have your api key in an environment variable called OPENAI_API_KEY
.
First, define an enum to hold the possible functions, with each variant being a function. The fields on these variants indicate the required arguments, and each field must also be an enum. The variants of these fields determine the allowed choices that can be passed to OpenAI's API. For example, here's a function definition for getting current weather:
Each argument must derive EnumDescriptor and VariantDescriptor, and must have the attribute macro arg_description. For example, a Location
argument might look like this:
Then, you can use these definitions to construct a request to the OpenAI API. The thing to note here is that the user prompt asks about the weather at the center of the universe, Swainsboro, GA, which doesn't correspond to any valid locations we provided it, and it returns the closest valid option, Atlanta.
In this examle the prompt also asks for the weather in two additional locations. Because I'm using a model that supports "parallel tool calls", it detects that it can make these three calls all at once and does so.
async
It is important to note that the call to "run" above is defined by the RunCommand trait and you must implement it, and you need a "GPT" variant that takes a String prompt.
This creates a request with the GetCurrentWeather
function, and two arguments: Location
and TemperatureUnits
.
Integration with clap:
Depending on how your existing clap application is structured, this library can provide an easy mechanism to allow use of your command line tool with natural language instructions. It supports value type arguments and enums. How well it performs will depend on which model you use, the system messages, and function descriptions.
If your application follows the pattern where you have an enum that derives clap's Subcommand
then this library can be added with little friction. This example has a variant CallMultiStep
, that is there just to demonstrate handling multiple sequential or parallel steps at once.
A word of caution: Recursion and AI probably aren't a good combo without guarding against it running away from you. It is entirely possible to make a prompt using the example below that will keep making requests to OpenAI.
#[derive(Parser)]
#[clap(author, version, about, long_about = None)]
#[clap(propagate_version = true)]
struct Cli {
#[clap(subcommand)]
command: Commands,
}
#[derive(Debug, Subcommand, ToolSet)]
pub enum Commands {
/// Adds two numbers
Add {
a: f64,
b: f64,
rounding_mode: RoundingMode,
},
/// Subtracts two numbers
Subtract {
a: f64,
b: f64,
rounding_mode: RoundingMode,
},
/// Multiplies two numbers
Multiply {
a: f64,
b: f64,
rounding_mode: RoundingMode,
},
/// Divides two numbers
Divide {
a: f64,
b: f64,
rounding_mode: RoundingMode,
},
/// CallMultiStep is designed to efficiently process complex, multi-step user requests. It takes an array of text prompts, each detailing a specific step in a sequential task. This function is crucial for handling requests where the output of one step forms the input of the next. When constructing the prompt list, consider the dependency and order of tasks. Independent tasks within the same step should be consolidated into a single prompt to leverage parallel processing capabilities. This function ensures that multi-step tasks are executed in the correct sequence and that all dependencies are respected, thus faithfully representing and fulfilling the user's request."
CallMultiStep {
prompt_list: Vec<String>,
},
GPT {
prompt: String,
},
}
The library provides a trait called "RunCommand", which makes you implement a "run" function. This function returns a result of Option, and this is only for cases where you have more than one step. In this example I'm showing how you can have value type arguments, as well as enums. If you want to define an enum that will serve as an argument to function calls, they need to derive clap's ValueEnum
, as well as the other EnumDescriptor
and VariantDescriptors
provided by this library.
Parallel Tool Calls
Currently the "run" function for RunCommand takes a single argument called that is an enum ToolCallExecutionStrategy. This sets how parallel tool calls will get executed if a prompt results in more than one. Running with ToolCallExecutionStrategy::Async
will run each tool call it can concurrently and this is what should be used is most cases. For now at least, selecting "Parallel" will run just the initial parallel calls on their own os threads. Subsequent parallel calls made in the course of a multi-step request will not spawn new os threads and they will run concurrently.
Embeddings
If you really want to do your part in hastening the end of humanity, you are going to want to build a really featureful system with lots of things that the LLM can do. This will create a problem for you as LLM's mind is like a butterfly. Even if the context window can hold it, give it too much to choose from and it will become more unreliable that it already is.
But even if your set of functions isn't that large, how well it does can be really
async