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

Function calling for OpenAI backend #573

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

Yiyun-Liang
Copy link
Collaborator

@Yiyun-Liang Yiyun-Liang commented Jun 29, 2024

Adding skeleton code for function calling with Open API models.
Example output (when tool_choice is "auto" or "required"):

system : You are a helpful assistant.
user : What's the weather like in San Francisco, Tokyo, Paris, and Beijing?
assistant : The current weather in San Francisco is 72°F, in Tokyo it is 10°C, and in Paris it is 22°C. Unfortunately, I couldn't retrieve the weather information for Beijing at the moment.

Example output (when tool_choice is "none"):

system : You are a helpful assistant.
user : What's the weather like in San Francisco, Tokyo, Paris, and Beijing?
assistant : I'm sorry for the inconvenience, but as an AI model developed by OpenAI, I don't have real-time capabilities to provide the current weather in San Francisco, Tokyo, Paris, and Beijing. I recommend using a reliable weather forecast service or website to get real-time weather updates for these cities.

The current implementation does not support:

  • chat models not listed by OpenAI as of 07/25/24.
  • non-chat models
  • speculative execution

Copy link
Member

@Ying1123 Ying1123 left a comment

Choose a reason for hiding this comment

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

Thanks for the PR! I left a few comments. The review is still in progress.

examples/quick_start/openai_example_func_call.py Outdated Show resolved Hide resolved
@@ -23,6 +23,7 @@
SglFunction,
SglGen,
SglImage,
SglFuncCall,
Copy link
Member

Choose a reason for hiding this comment

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

use dictionary order instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removing this given we are moving it to be part of SglGen.

def multi_turn_question(s, question_1, functions=[]):
s += sgl.system("You are a helpful assistant.")
s += sgl.user(question_1)
s += sgl.func_call("func_call_1", tools=functions, tool_choice="auto")
Copy link
Member

Choose a reason for hiding this comment

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

We may also want to retrieve the results from function call.
Add tests for state["func_call_1"] in the function single().

# Open AI model requires function call information to be sent to the model
# along with the prompt.
for function_call in s.function_calls:
prompt.append(function_call)
else:
Copy link
Member

@Ying1123 Ying1123 Jun 30, 2024

Choose a reason for hiding this comment

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

s.messages_ should be updated after function call finished rather than in generate, and the append logic should happen in interpreter.py, see _execute_role_end() as a reference.

Additionally, changes prompt implicitly changes s.messages_. This is not safe. Changes s.messages_ then set prompt = s.messages_ is better.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Restructured the code a little bit based on your suggestions (with some minor tweaks but I can update if you think it's still better to move the function call generate call outside of generate (we will just have a simpler generate call):

Within openai.py

  • build_function_call_messages(): a new function which builds function call messages. Given function signature is specific to open ai models, keeping the logic to parse inputs and produce function call messages within the backend code.
  • generate(): Given prompt is local to the generate() call, I directly added function_call_messages to it so that we can call with function call messages during the current completion call's prompt. The main intuition is to try resuing the generate call logic and it also only appends function call response (comp) without intermediate messages into the final text/messages.

Within interpreter.py

  • Updated _execute_gen() logic to include building function call messages if tools are provided, and handle both parallel function calling and non-parallel function calling by either calling backend.generate one time for parallel function call supported models, or multiple times if parallel call is not supported.

"gpt-3.5-turbo-0613",
]:
raise RuntimeError(
"This model currently does not support function calling."
Copy link
Member

Choose a reason for hiding this comment

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

keep in mind that the set of models that support function calling and parallel function calling are different.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for pointing out! Updated to have different handling logic.

cur_tool_choice = (
tool_choice
if tool_choice in ["auto", "required", "none"]
else {"type": "function", "function": {"name": tool_choice}}
Copy link
Member

Choose a reason for hiding this comment

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

In this case, assert tool_choice is in names of candidate functions.

Comment on lines 318 to 342
tool_calls = response_message.tool_calls
# Check if the model wanted to call a function
ret_messages = []
if tool_calls:
# Call the function
# Note: the JSON response may not always be valid; be sure to handle errors
available_functions = {}
for tool in tools:
available_functions[tool.__name__] = tool
ret_messages.append(response_message)
# Send the info for each function call and function response to the model
for tool_call in tool_calls:
function_name = tool_call.function.name
function_to_call = available_functions[function_name]
function_args = json.loads(tool_call.function.arguments)
function_response = function_to_call(**function_args)
ret_messages.append(
{
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": str(function_response),
}
)
return ret_messages
Copy link
Member

Choose a reason for hiding this comment

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

I think it is better to put the logic of real function call into the interpreter, so that it can be reused when we develop the feature for local models.
And remember to handle the logic of appending s.messages_ and s.text_ in the interpreter.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Makes sense, returning just the function call messages here so we can do real function call separately.

@@ -554,6 +560,12 @@ def _execute_select(self, expr: SglSelect):
self.variable_event[name].set()
self.text_ += decision

def _execute_func_call(self, expr: SglFuncCall):
# TODO: Should we clear the previous function call states for the next function call
Copy link
Member

Choose a reason for hiding this comment

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

I think yes, by default. Although accumulating functions could be an option.

@Ying1123 Ying1123 self-assigned this Jun 30, 2024
@Ying1123 Ying1123 changed the title Func call Function calling for OpenAI backend Jun 30, 2024
def multi_turn_question(s, question_1, functions=[]):
s += sgl.system("You are a helpful assistant.")
s += sgl.user(question_1)
s += sgl.func_call("func_call_1", tools=functions, tool_choice="auto")
Copy link
Member

Choose a reason for hiding this comment

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

A design suggestion is that it might be better to just have sgl.gen with func_call as an argument.

Copy link
Collaborator Author

@Yiyun-Liang Yiyun-Liang Jun 30, 2024

Choose a reason for hiding this comment

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

I agree, I think it's more straightforward to have it as part of sgl.gen. Would it make sense to have something like sgl.gen("answer_1", max_tokens=256, sgl.func_call(...)) or simply expose parameters directly to sgl.gen like sgl.gen("answer_1", max_tokens=256, tools=[...])?

Copy link
Member

Choose a reason for hiding this comment

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

Let's simply expose parameters directly to sgl.gen.

@Yiyun-Liang Yiyun-Liang force-pushed the func-call branch 3 times, most recently from f58f983 to 1e978c2 Compare July 26, 2024 03:45
@Yiyun-Liang Yiyun-Liang force-pushed the func-call branch 2 times, most recently from 16b1dbf to f1389dc Compare July 26, 2024 04:13
@merrymercy
Copy link
Contributor

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.

3 participants