Skip to content

Commit

Permalink
Function calling upgrade (#1443)
Browse files Browse the repository at this point in the history
* function calling upgraded: async/sync mixing works now for all combinations and register_function added to simplify registration of functions without decorators

* polishing

* fixing tests

---------

Co-authored-by: Eric Zhu <[email protected]>
Co-authored-by: Chi Wang <[email protected]>
  • Loading branch information
3 people authored Jan 31, 2024
1 parent 0107b52 commit a2d4b47
Show file tree
Hide file tree
Showing 9 changed files with 816 additions and 210 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,6 @@ test/test_files/agenteval-in-out/out/
# Files created by tests
*tmp_code_*
test/agentchat/test_agent_scripts/*

# test cache
.cache_test
7 changes: 4 additions & 3 deletions autogen/agentchat/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from .agent import Agent
from .assistant_agent import AssistantAgent
from .conversable_agent import ConversableAgent
from .conversable_agent import ConversableAgent, register_function
from .groupchat import GroupChat, GroupChatManager
from .user_proxy_agent import UserProxyAgent

__all__ = [
__all__ = (
"Agent",
"ConversableAgent",
"AssistantAgent",
"UserProxyAgent",
"GroupChat",
"GroupChatManager",
]
"register_function",
)
64 changes: 58 additions & 6 deletions autogen/agentchat/conversable_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -914,9 +914,20 @@ def generate_function_call_reply(
func_call = message["function_call"]
func = self._function_map.get(func_call.get("name", None), None)
if inspect.iscoroutinefunction(func):
return False, None

_, func_return = self.execute_function(message["function_call"])
try:
# get the running loop if it was already created
loop = asyncio.get_running_loop()
close_loop = False
except RuntimeError:
# create a loop if there is no running loop
loop = asyncio.new_event_loop()
close_loop = True

_, func_return = loop.run_until_complete(self.a_execute_function(func_call))
if close_loop:
loop.close()
else:
_, func_return = self.execute_function(message["function_call"])
return True, func_return
return False, None

Expand All @@ -943,7 +954,9 @@ async def a_generate_function_call_reply(
func = self._function_map.get(func_name, None)
if func and inspect.iscoroutinefunction(func):
_, func_return = await self.a_execute_function(func_call)
return True, func_return
else:
_, func_return = self.execute_function(func_call)
return True, func_return

return False, None

Expand All @@ -968,8 +981,20 @@ def generate_tool_calls_reply(
function_call = tool_call.get("function", {})
func = self._function_map.get(function_call.get("name", None), None)
if inspect.iscoroutinefunction(func):
continue
_, func_return = self.execute_function(function_call)
try:
# get the running loop if it was already created
loop = asyncio.get_running_loop()
close_loop = False
except RuntimeError:
# create a loop if there is no running loop
loop = asyncio.new_event_loop()
close_loop = True

_, func_return = loop.run_until_complete(self.a_execute_function(function_call))
if close_loop:
loop.close()
else:
_, func_return = self.execute_function(function_call)
tool_returns.append(
{
"tool_call_id": id,
Expand Down Expand Up @@ -1986,3 +2011,30 @@ def get_total_usage(self) -> Union[None, Dict[str, int]]:
return None
else:
return self.client.total_usage_summary


def register_function(
f: Callable[..., Any],
*,
caller: ConversableAgent,
executor: ConversableAgent,
name: Optional[str] = None,
description: str,
) -> None:
"""Register a function to be proposed by an agent and executed for an executor.
This function can be used instead of function decorators `@ConversationAgent.register_for_llm` and
`@ConversationAgent.register_for_execution`.
Args:
f: the function to be registered.
caller: the agent calling the function, typically an instance of ConversableAgent.
executor: the agent executing the function, typically an instance of UserProxy.
name: name of the function. If None, the function name will be used (default: None).
description: description of the function. The description is used by LLM to decode whether the function
is called. Make sure the description is properly describing what the function does or it might not be
called by LLM when needed.
"""
f = caller.register_for_llm(name=name, description=description)(f)
executor.register_for_execution(name=name)(f)
213 changes: 136 additions & 77 deletions notebook/agentchat_function_call.ipynb

Large diffs are not rendered by default.

Loading

0 comments on commit a2d4b47

Please sign in to comment.