11 个不稳定版本 (3 个破坏性更新)
0.4.0 | 2024 年 3 月 11 日 |
---|---|
0.3.1 | 2024 年 3 月 1 日 |
0.2.4 | 2024 年 2 月 2 日 |
0.2.3 | 2024 年 1 月 30 日 |
0.1.2 | 2023 年 7 月 27 日 |
#317 在 命令行界面
每月 457 次下载
415KB
402 行
openai-func-enums
openai-func-enums 是一个非官方的 Rust 库,用于 OpenAI。它包含一组过程宏和其他函数,与 async-openai 一起使用,可以轻松地使用枚举来组合“函数”工具类型,这些类型可以传递给 OpenAI 的聊天完成 API。
如果您想查看一个更大的示例,其中函数定义比上下文窗口多,请查看 dripgrep。
为什么?
这个项目的动机是利用 OpenAI 函数调用进行逻辑控制流。如果您有很多“函数调用”要处理,特别是如果它们具有共享的参数类型,那么仅使用 async-openai 来完成这项工作将是笨拙的。这个库允许将返回值反序列化为宏生成的结构的实例,这样就可以轻松地取回响应并匹配由模型选择的变体。
特性
-
枚举是最大的: openai-func-enums 要求您定义一个枚举来表示要传递给 OpenAI API 的可能“函数”,每个变体代表一个函数,这些变体上的字段表示所需的参数。每个字段可以是枚举、值类型或值类型的向量,枚举字段的变体确定可以传递给 OpenAI API 的允许选择。
-
Token 计数: 该库记录通过枚举定义的每个“函数”关联的 token 计数。
-
基于嵌入的函数过滤:使用功能
--compile_embeddings_all
构建您的应用程序,将为您的函数获取嵌入并将其烘焙到运行时可用的不复制存档中。将使用每个请求的令牌预算来限制工具只与提示最相似的内容。您还可以指定必须包含的函数,无论其相似度等级如何。已定义一个仅更新更改内容的特征标志,但尚未实现。它使用 rkyv(零复制反序列化框架)进行序列化和反序列化。 -
clap-gpt:这个库提供了宏和特质,允许您将现有的 clap 应用程序转换为 clap-gpt 应用程序,而无需进行大量额外的仪式。请参阅使用部分以获取示例。
-
并行工具调用:如果 OpenAI 选择了同时调用多个可用工具,则此库将根据您指定的执行策略处理它们。它可以异步、同步或根据您的需要在线程上运行。clap 集成示例进一步详细说明了并行工具调用。
使用方法
**注意:此库需要 async-openai,它要求您在名为 OPENAI_API_KEY
的环境变量中具有您的 API 密钥。
环境
有几个环境变量需要设置。我建议使用 build.rs
文件来设置这些变量。例如,build.rs
文件显示您需要的最小内容。如果您想要基于嵌入的函数剔除,则需要额外的两个变量,它们在 clap-integration
示例的 build.rs
文件中显示。
FUNC_ENUMS_EMBED_PATH
:存储嵌入数据的路径(包括文件名)。FUNC_ENUMS_EMBED_MODEL
:用于嵌入的模型名称。FUNC_ENUMS_MAX_RESPONSE_TOKENS
FUNC_ENUMS_MAX_REQUEST_TOKENS
FUNC_ENUMS_MAX_FUNC_TOKENS
FUNC_ENUMS_SINGLE_ARG_TOKENS
:目前这没有任何作用,但将会
特征标志
请检查示例中的 Cargo.toml
文件。这些宏需要您进行设置。例如,get-current-weather
示例未利用与嵌入/函数剔除相关的功能,而 clap-integration 示例则使用了这些功能。
示例
首先,定义一个枚举来存储可能的功能,每个变体都是一个函数。这些变体的字段表示所需的参数,每个字段也必须是一个枚举。这些字段的变体决定了可以传递给 OpenAI API 的允许选择。例如,以下是获取当前天气的函数定义:
#[derive(Debug, ToolSet)]
pub enum FunctionDef {
/// "Get the current weather in the location closest to the one provided location"
GetCurrentWeather {
location: Location,
temperature_units: TemperatureUnits,
},
GPT {
prompt: String,
},
}
每个参数必须派生自 EnumDescriptor
和 VariantDescriptors
,并且必须具有属性宏 arg_description
。例如,一个 Location
参数可能看起来像这样:
#[derive(Clone, Debug, Deserialize, EnumDescriptor, VariantDescriptors)]
#[arg_description(description = "The only valid locations that can be passed.")]
pub enum Location {
Atlanta,
Boston,
// ...
}
然后,您可以使用这些定义来构造对 OpenAI API 的请求。这里需要注意的是,用户提示询问的是位于宇宙中心的 Swainsboro, GA 的天气,这并不对应我们提供的任何有效位置,并且它返回最近的合法选项,亚特兰大。
在此示例中,提示还要求提供另外两个地点的天气。因为我使用的是一个支持“并行工具调用”的模型,它检测到它可以同时进行这三个调用,所以它这样做。
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (sender, receiver) = mpsc::channel(100);
let logger = Arc::new(Logger { sender });
spawn(logger_task(receiver));
let logger_clone = logger.clone();
let system_message = Some((String::from("You are an advanced function-calling bot."), 9));
// The enum that derives TooSet needs to have a GPT variant.
(FunctionDef::GPT {
prompt: "What's the weather like in Swainsboro, GA, Nashville, TN, Los Angeles, CA?"
.to_string(),
})
.run(
ToolCallExecutionStrategy::Async,
None,
logger_clone,
system_message,
)
.await
.map_err(|e| {
Box::new(CommandError::new(&format!(
"Command failed with error: {}",
e
)))
})?;
Ok(())
}
这创建了一个带有 GetCurrentWeather
函数的请求,以及两个参数:Location
和 TemperatureUnits
。
请注意,上面对run
的调用是由RunCommand
特性定义的,您必须实现它,并且您需要一个接受字符串提示的GPT
变体。在某个时候,我可能需要一个通用的“exclude_function”属性宏,让您指定不应显示给模型的函数,并且这些函数实际上只应由用户/客户端调用。这个问题将在下一个例子中进行讨论。
与clap的集成
根据您现有的clap应用程序的结构,这个库可以提供一个简单的机制,允许您使用自然语言指令使用您的命令行工具。它支持值类型参数和枚举。它的表现将取决于您使用的模型、系统消息和功能描述。
如果您的应用程序遵循枚举派生自clap的Subcommand
的模式,则可以轻松地添加这个库。这个例子有一个CallMultiStep
变体,它只是为了演示如何一次性处理多个顺序或并行步骤。
警告:拥有特殊GPT
变体的原因是防止系统陷入无法停止的来回对话。
#[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,
},
}
并行工具调用
RunCommand
的run
函数接受一个类型为ToolCallExecutionStrategy
的参数。这设置了如果有多个提示导致的结果,并行工具调用将如何执行。使用ToolCallExecutionStrategy::Async
运行将并发运行每个工具调用,在大多数情况下应该使用这种方法。至少目前,选择Parallel
将仅在它们的操作系统线程上运行初始的并行调用。在多步骤请求过程中做出的后续并行调用将不会创建新的操作系统线程,并将并发运行。Sync
变体将按顺序执行所有操作。
嵌入
如果您真的想加快人类灭亡的步伐,您将需要构建一个功能丰富的系统,其中包含许多LLM可以执行的事情。这将会给您带来问题,因为LLM的思维方式就像蝴蝶。即使上下文窗口可以容纳它,给它提供太多的选择,它将比现在更加不可靠。您需要设置环境变量,如示例中的build.rs
文件所示。您还需要像这样编译:cargo build --release --features "compile_embeddings_all"
。请仔细注意示例Cargo.toml
文件的设置,特别是关于功能标志的。
- 注意:如果您启用
function_filtering
功能但未先编译嵌入,您将会非常困惑。在更改或添加功能时,不要忘记相关向量的状态。
必需的特质实现
该库提供了一个名为 RunCommand
的特质,它允许您实现一个 "run" 函数。此函数返回一个 Option 类型的结果,这仅适用于您有多个步骤的情况。在这个例子中,我将展示您如何使用值类型参数以及枚举。如果您想定义一个作为函数调用参数的枚举,它们需要派生 clap 的 ValueEnum
,以及该库提供的其他 EnumDescriptor
和 VariantDescriptors
。
#[async_trait]
impl RunCommand for Commands {
async fn run(
&self,
execution_strategy: ToolCallExecutionStrategy,
_arguments: Option<Vec<String>>,
logger: Arc<Logger>,
system_message: Option<(String, usize)>,
) -> Result<
(Option<String>, Option<Vec<String>>),
Box<dyn std::error::Error + Send + Sync + 'static>,
> {
let max_response_tokens = 1000_u16;
let request_token_limit = 4191;
let model_name = "gpt-4-1106-preview";
match self {
Commands::Add {
a,
b,
rounding_mode,
} => {
let result = rounding_mode.round(a + b);
println!(
"Result of adding {} and {} with rounding mode {:#?}: {}",
a,
b,
rounding_mode.variant_name_with_token_count().0,
result
);
return Ok((Some(result.to_string()), None));
}
Commands::Subtract {
a,
b,
rounding_mode,
} => {
let result = rounding_mode.round(a - b);
println!(
"Result of subtracting {} from {} with rounding mode {:#?}: {}",
b,
a,
rounding_mode.variant_name_with_token_count().0,
result
);
return Ok((Some(result.to_string()), None));
}
Commands::Multiply {
a,
b,
rounding_mode,
} => {
let result = rounding_mode.round(a * b);
println!(
"Result of multiplying {} and {} with rounding mode {:#?}: {}",
a,
b,
rounding_mode.variant_name_with_token_count().0,
result
);
return Ok((Some(result.to_string()), None));
}
Commands::Divide {
a,
b,
rounding_mode,
} => {
if *b != 0.0 {
let result = rounding_mode.round(a / b);
println!(
"Result of dividing {} by {} with rounding mode {:#?}: {}",
a,
b,
rounding_mode.variant_name_with_token_count().0,
result
);
return Ok((Some(result.to_string()), None));
} else {
return Err(Box::new(CommandError::new("Cannot divide by zero")));
}
}
Commands::CallMultiStep { prompt_list } => {
let _ = logger
.sender
.send(String::from("this is the prompt list"))
.await;
let message = format!("{:#?}", prompt_list);
let _ = logger.sender.send(message).await;
let prior_result = Arc::new(Mutex::new(None));
let command_args_list: Vec<String> = Vec::new();
let command_args = Arc::new(Mutex::new(Some(command_args_list)));
for (i, prompt) in prompt_list.iter().enumerate() {
let prior_result_clone = prior_result.clone();
let command_args_clone = command_args.clone();
let logger_clone = logger.clone();
match i {
0 => {
CommandsGPT::run(
&prompt.to_string(),
model_name,
Some(request_token_limit),
Some(max_response_tokens),
system_message.clone(),
prior_result_clone,
execution_strategy.clone(),
command_args_clone,
None,
None,
logger_clone,
)
.await?
}
_ => {
let prior_result_guard = prior_result.lock().await;
if let Some(prior) = &*prior_result_guard {
let new_prompt =
format!("The prior result was: {}. {}", prior.clone(), prompt);
drop(prior_result_guard);
CommandsGPT::run(
&new_prompt,
model_name,
Some(request_token_limit),
Some(max_response_tokens),
system_message.clone(),
prior_result_clone,
execution_strategy.clone(),
command_args_clone,
None,
None,
logger_clone,
)
.await?
} else {
*prior_result.lock().await = None;
}
}
}
}
let result = String::from("Ok.");
return Ok((Some(result), None));
}
Commands::GPT { prompt } => {
let prompt_embedding = single_embedding(prompt, FUNC_ENUMS_EMBED_MODEL).await?;
let prior_result = Arc::new(Mutex::new(None));
let command_args = Arc::new(Mutex::new(None));
let embed_path = Path::new(FUNC_ENUMS_EMBED_PATH);
let mut ranked_func_names = vec![];
let logger_clone = logger.clone();
if embed_path.exists() {
let mut file = File::open(embed_path).unwrap();
let mut bytes = Vec::new();
file.read_to_end(&mut bytes).unwrap();
let archived_funcs =
rkyv::check_archived_root::<Vec<FuncEmbedding>>(&bytes).unwrap();
ranked_func_names = rank_functions(archived_funcs, prompt_embedding).await;
}
let required_funcs = vec![String::from("CallMultiStep")];
CommandsGPT::run(
prompt,
model_name,
Some(request_token_limit),
Some(max_response_tokens),
system_message,
prior_result,
execution_strategy.clone(),
command_args,
Some(ranked_func_names),
Some(required_funcs),
logger_clone,
)
.await?;
}
};
Ok((None, None))
}
}
#[derive(Clone, Debug, Deserialize, EnumDescriptor, VariantDescriptors, ValueEnum)]
#[arg_description(description = "Different modes to round a number.")]
pub enum RoundingMode {
NoRounding,
Nearest,
Zero,
Up,
Down,
}
impl RoundingMode {
pub fn round(&self, number: f64) -> f64 {
match *self {
RoundingMode::NoRounding => number,
RoundingMode::Nearest => number.round(),
RoundingMode::Zero => number.trunc(),
RoundingMode::Up => number.ceil(),
RoundingMode::Down => number.floor(),
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (sender, receiver) = mpsc::channel(100);
let logger = Arc::new(Logger { sender });
spawn(logger_task(receiver));
let logger_clone = logger.clone();
let system_instructions = Some((
String::from(
"You are an advanced function-calling bot, adept at handling complex, \
multi-step user requests. Your role is to discern and articulate \
each step of a user's request, especially when it involves sequential \
operations. Use the CallMultiStep function for requests that require \
sequential processing. Each step should be described in a separate \
prompt, with attention to whether the steps are independent or \
interdependent. For interdependent steps, ensure each prompt \
accurately represents the sequence and dependencies of the tasks. \
Remember, a single step may encompass multiple tasks that can be \
executed in parallel. Your goal is to capture the entire scope of the \
user's request, structuring it into an appropriate sequence of function \
calls without omitting any steps. For example, if a user asks to add 8 \
and 2 in the first step, and then requests the result to be multiplied \
by 7 and 5 in separate tasks of the second step, use CallMultiStep with \
two prompts: the first for addition, and the second combining both \
multiplication tasks, recognizing their parallel nature.",
),
7_usize,
));
let cli = Cli::parse();
let start_time = Instant::now();
cli.command
.run(
ToolCallExecutionStrategy::Async,
None,
logger_clone,
system_instructions,
)
.await
.map_err(|e| {
Box::new(CommandError::new(&format!(
"Command failed with error: {}",
e
)))
})?;
let duration = start_time.elapsed();
println!("Command completed in {:.2} seconds", duration.as_secs_f64());
Ok(())
}
依赖项
~23–39MB
~459K SLoC