Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR 2634 CI - Fix the tool_choice format for named choice by adapting OpenAIs scheme #2645

Merged
merged 15 commits into from
Nov 19, 2024

Conversation

drbh
Copy link
Collaborator

@drbh drbh commented Oct 14, 2024

This PR re opens #2634 to run CI

@drbh
Copy link
Collaborator Author

drbh commented Oct 14, 2024

@linusbierhoff thank you for the contribution! 🙏 I've opened this PR to run CI, and add some other tool calling improvements along with your changes.

This PR now removes the old ToolType in favor of a more simple ChatCompletionToolChoiceOption. This new struct includes the addition of TypedChoice and correctly allows a user to specify the following tools as strings or as an object. Additionally this PR includes support for the required tool_choice, which forces the model to use one of the user provided tools. Lastly the new struct also improves the openapi spec and makes the tool choice a oneOf which improves the spec readability

@drbh drbh force-pushed the pr-2634-ci-branch branch from 37a1239 to 970d8dc Compare October 14, 2024 19:08

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Default)]
#[serde(from = "ToolTypeDeserializer")]
pub enum ChatCompletionToolChoiceOption {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid java naming please ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea I can revert the name change if thats better.

The name change is to align with the naming that OpenAI is using in their spec. https://github.com/openai/openai-openapi/blob/d033c364c6574068ee89f3d5f845a4830bddd503/openapi.yaml#L9581, I aligned the names because I thought it may be helpful in automatically checking the spec, however it is a very long name..

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it may be helpful in automatically checking the spec

Then let's wait for it to be actually relevant, it's not usually a good thing to do things "because we might need it". Either you need it or you don't.

Personally I don't think it has any relevance to use their names.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it that make sense, I've changed the name to ToolChoice in the latest commit. (its much cleaner 👍)

NoTool,
/// Means the model must call one or more tools.
#[schema(rename = "required")]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

schema rename is not what you want, you want serde rename most likely.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh I actually included it to improve the generated schema

this macro changes the enum on the schema to show the expected lowercase text instead of Required

{
  "type": "string",
  "description": "Means the model must call one or more tools.",
  "enum": [
    "required"
  ]
},

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and serde(renamed) actually modifies what the input is from the user.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually a better serde(rename) on the whole struct should fix every field (both schema and actual JSON parsing)

Copy link
Collaborator Author

@drbh drbh Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yea thats much more concise 🙏, updated in the latest commit

/// Forces the model to call a specific tool.
#[schema(rename = "function")]
#[serde(alias = "function")]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have link in rustdoc to where all these attributes are explained, possibly in the original openai spec (or anthropic)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, just added a useful link above the enum that explains all 4 of the options in OpenAI's docs this link will also be included in the openapi schema for the ChatCompletionToolChoiceOption description


let named = r#"{"tool_choice":"myfn"}"#;
let de_named: TestRequest = serde_json::from_str(named).unwrap();
assert_eq!(de_named.tool_choice, ref_choice);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert_eq!(de_named.tool_choice, ref_choice);
assert_eq!(de_named.tool_choice,ChatCompletionToolChoiceOption::Function(FunctionName {
name: "myfn".to_string(),
}) );

Please avoid indirection in tests. Tests are about being as dumb as possible, so a reader can easily assess if the test is correct, or outdated. Every indirection makes that harder to reason about.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great points, indirection removed in the latest commit

tools.push(no_tool);
// add the no_tool function to the tools as long as we are not required to use a specific tool
if tool_choice != ChatCompletionToolChoiceOption::Required {
let no_tool = Tool {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this only ever used in the auto branch ? Wouldn't putting it in the match branch make more sense ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep good point. Just moved into the match branch and removed the let mut tools and only mutate tools if the Auto option is chosen. thanks

ToolType::OneOf => tools.clone(),
ToolType::NoTool => return Ok((tools, None)),
ChatCompletionToolChoiceOption::Required => tools.clone(),
ChatCompletionToolChoiceOption::Auto => tools.clone(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this .clone needed ? I know it was there before, but since we're upgrading this, couldn't we get rid of it ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes agreed they should be removed. Just removed all of the tool clones above and improved the match in general

ToolType::NoTool => return Ok((tools, None)),
ChatCompletionToolChoiceOption::Required => tools.clone(),
ChatCompletionToolChoiceOption::Auto => tools.clone(),
ChatCompletionToolChoiceOption::NoTool => return Ok((tools, None)),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I think early returns in match branches are bug nests, it's easy to overlook those branches, returning an empty slice and letting the rest roll seems slightly better (code reading wise)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, updated in the latest to commit to discard the tools and continue executing the function normally

#[schema(nullable = true, example = "null")]
pub tool_choice: ToolChoice,
#[schema(nullable = true, default = "null", example = "null")]
pub tool_choice: Option<ChatCompletionToolChoiceOption>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the change to Option you have the NoTool choice, there shouldn't be a need to Option wrapping is there ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option is needed so the user can send a http request without a tool_choice param included. If tool_choice is None (user did not include a tool_choice in the request), ChatCompletionToolChoiceOption defaults to NoTool

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't a default take care of that ? And shouldn't the default be "auto"?

@@ -906,8 +906,18 @@ impl ChatRequest {
let (inputs, grammar, using_tools) = prepare_chat_input(
infer,
response_format,
tools,
tool_choice,
tools.clone(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No clone.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed, it was previously required because of a check of the tools vec that was below this line (moved it up and removed the clone). thanks

tool_choice,
tools.clone(),
// unwrap or default (use "auto" if tools are present, and "none" if not)
tool_choice.map_or_else(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should be already done in the deserialization, there are about 3 different function calls to preprare_chat_input.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, in the latest commit i've moved the logic from prepare_chat_input into try_into_generate and remove the prepare_chat_input all together.

@Narsil
Copy link
Collaborator

Narsil commented Oct 15, 2024

required is that an OpenAI spec ?

Can you point to the original references for the things we're adding.
Also dropping the old code is a pretty bad breaking change, it should be in big bold red !.

Why are we dropping support for the old way, if it was there, is it documented anywhere ? Should we update the old doc ?

This PR is also seriously lacking integration tests for all of these. The deserialization tests are nice, but not enough.

@drbh
Copy link
Collaborator Author

drbh commented Oct 15, 2024

required is that an OpenAI spec ?

yea required is supported by OpenAI, see docs here: https://platform.openai.com/docs/guides/function-calling/configuring-function-calling-behavior-using-the-tool_choice-parameter. Note** OpenAI added this parameter after TGI's initial tool implementation (hence why it was missing), additionally prior to #2614 auto and required had the same functionality in TGI, however now its more pressing/great to have the api's aligned.

Also dropping the old code is a pretty bad breaking change, it should be in big bold red !.

yea regarding the removed code - as the original PR noted, the function tool choice was expecting the incorrect format. I believe I confused some of the function calling formats and ended up adding one that was not aligned with any major LLM api.

regarding flagging the breaking changes, would it be best to include the old API but throw a helpful error if it is used? Currently I've removed the path in favor of the corrected one.

Why are we dropping support for the old way, if it was there, is it documented anywhere ? Should we update the old doc ?

we're dropping support since it was incorrectly implemented, we do not have documentation specific to this tool choice, however I'm going to push another commit with updated docs for the supported tool choice types

This PR is also seriously lacking integration tests for all of these. The deserialization tests are nice, but not enough.

just added a test for required and one for a function object in test_tools_llama.py

@drbh drbh force-pushed the pr-2634-ci-branch branch from e1d1706 to 79a67aa Compare October 15, 2024 16:36
@HuggingFaceDocBuilderDev

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

Comment on lines 909 to 912
ChatCompletionToolChoiceOption::Auto
} else {
ChatCompletionToolChoiceOption::NoTool
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shoudln't Auto be enough in all cases ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if tool_choice is "required" and no tools are sent ?

("auto", tools = []) we should just ignore the grammar right ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great point! ToolChoice::Auto should be enough and in the case there are no tools we'll ignore the grammar/tool logic. updated in the latest commit

"tools": tools,
"tool_choice": {
"type": "function",
"function": {"name": "get_current_weather"},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't choosing the other tools be a better test ? (Forcing to check that the other tool was used that in the required test case ?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, just updated to choose get_n_day_weather_forecast instead

last_response = response

assert count == 30
print(tool_calls_generated)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops 😬, removed in latest commit. thanks!

Comment on lines 25 to 26
if tools.is_empty() {
return Ok((tools, None));
return Ok((Vec::with_capacity(0), None));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vec![] ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated in latest commit

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let tools_to_use = match tool_choice was updated to return a vec![] rather than a Vec::with_capacity(0), and the conditional was moved below tools_to_use to return none in both cases (no tools supplied, or if NoTools specified)

ToolChoice::NoTool => vec![],

@@ -850,13 +850,13 @@ pub(crate) struct ChatRequest {
/// A specific tool to use. If not provided, the model will default to use any of the tools provided in the tools parameter.
#[serde(default)]
#[schema(nullable = true, default = "null", example = "null")]
pub tool_choice: Option<ChatCompletionToolChoiceOption>,
pub tool_choice: Option<ToolChoice>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't default "auto" allow removing the Option here ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yes that worked, I was originally under the impression the Option was still needed to handle all http request but this is much cleaner. Thank you


/// Response format constraints for the generation.
///
/// NOTE: A request can use `response_format` OR `tools` but not both.
#[serde(default)]
#[schema(nullable = true, default = "null", example = "null")]
#[schema(nullable = true, default = "auto", example = "auto")]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you made the change to the wrong item, no ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes thank you, updated the correct argument in the latest commit

Auto,
/// Means the model will not call any tool and instead generates a message.
#[schema(rename = "none")]
#[default]
NoTool,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this no_tool with snake_case ?This should mean a rename of this property or None, no ?

I don't think schema(rename) imples a serde rename.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, updated to serde(rename) and added a test to ensure that the "none" values is respected correctly

@drbh drbh force-pushed the pr-2634-ci-branch branch from 193ad66 to c1eab6c Compare October 29, 2024 13:34
@drbh drbh requested a review from Narsil October 30, 2024 14:23
@drbh
Copy link
Collaborator Author

drbh commented Nov 19, 2024

merging optimistically as CI is green and issues have been resolved. Also mentioned in the internal #text-generation-inference Slack channel.

Will watch for any regressions and revert if needed

@drbh drbh merged commit 5489406 into main Nov 19, 2024
12 of 13 checks passed
@drbh drbh deleted the pr-2634-ci-branch branch November 19, 2024 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants