Skip to content

feat:Unify toolkit output adding status #2033

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 122 additions & 15 deletions camel/agents/chat_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,7 @@ def _execute_tool(
tool_call_request: ToolCallRequest,
) -> ToolCallingRecord:
r"""Execute the tool with arguments following the model's response.
Includes retry logic for failed tool calls (up to 3 attempts).

Args:
tool_call_request (_ToolCallRequest): The tool call request.
Expand All @@ -1270,31 +1271,137 @@ def _execute_tool(
args = tool_call_request.args
tool_call_id = tool_call_request.tool_call_id
tool = self._internal_tools[func_name]
try:
result = tool(**args)
except Exception as e:
# Capture the error message to prevent framework crash
error_msg = f"Error executing tool '{func_name}': {e!s}"
result = {"error": error_msg}
logging.warning(error_msg)

max_retries = 3
retries = 0
last_error = None
result = None

while retries < max_retries:
try:
result_with_status = tool(**args)

if (
isinstance(result_with_status, dict)
and "status" in result_with_status
):
if result_with_status["status"] == "success":
result = result_with_status["tool_call_output"]
break
else:
last_error = result_with_status.get(
"error_message", "Unknown error"
)
logging.warning(
f"Tool '{func_name}' failed: {last_error}"
)
retries += 1
if retries < max_retries:
logging.info(
f"Retrying tool '{func_name}' "
f"(attempt {retries}/{max_retries})"
)
continue
else:
result = result_with_status["tool_call_output"]
break
else:
result = result_with_status
break

except Exception as e:
last_error = f"Error executing tool '{func_name}': {e!s}"
logging.warning(last_error)
retries += 1
if retries >= max_retries:
result = {"error": last_error}
break
logging.info(
f"Retrying tool '{func_name}' "
f"(attempt {retries}/{max_retries})"
)

if retries == max_retries:
logging.error(
f"Tool '{func_name}' failed. "
f"Final error: {last_error}"
)
Comment on lines +1274 to +1328
Copy link
Collaborator

Choose a reason for hiding this comment

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

The overall logic seems to have no problem, but it is a bit complicated. Can it be optimized?


return self._record_tool_calling(func_name, args, result, tool_call_id)

async def _aexecute_tool(
self,
tool_call_request: ToolCallRequest,
) -> ToolCallingRecord:
r"""Execute the tool asynchronously.
Includes retry logic for failed tool calls (up to 3 attempts).

Args:
tool_call_request (_ToolCallRequest): The tool call request.

Returns:
FunctionCallingRecord: A struct for logging information about this
function call.
"""
func_name = tool_call_request.tool_name
args = tool_call_request.args
tool_call_id = tool_call_request.tool_call_id
tool = self._internal_tools[func_name]
try:
result = await tool.async_call(**args)
except Exception as e:
# Capture the error message to prevent framework crash
error_msg = f"Error executing async tool '{func_name}': {e!s}"
result = {"error": error_msg}
logging.warning(error_msg)

max_retries = 3
retries = 0
last_error = None
result = None

while retries < max_retries:
try:
result_with_status = await tool.async_call(**args)

if (
isinstance(result_with_status, dict)
and "status" in result_with_status
):
if result_with_status["status"] == "success":
result = result_with_status["tool_call_output"]
break
else:
last_error = result_with_status.get(
"error_message", "Unknown error"
)
logging.warning(
f"Tool '{func_name}' failed: {last_error}"
)
retries += 1
if retries < max_retries:
logging.info(
f"Retrying tool '{func_name}' "
f"(attempt {retries}/{max_retries})"
)
continue
else:
result = result_with_status["tool_call_output"]
break
else:
result = result_with_status
break

except Exception as e:
last_error = f"Error executing async tool '{func_name}': {e!s}"
logging.warning(last_error)
retries += 1
if retries >= max_retries:
result = {"error": last_error}
break
logging.info(
f"Retrying async tool '{func_name}' "
f"(attempt {retries}/{max_retries})"
)

if retries == max_retries:
logging.error(
f"Tool '{func_name}' failed. "
f"Final error: {last_error}"
)

return self._record_tool_calling(func_name, args, result, tool_call_id)

Expand Down Expand Up @@ -1380,4 +1487,4 @@ def __repr__(self) -> str:
"""
return (
f"ChatAgent({self.role_name}, {self.role_type}, {self.model_type})"
)
)
81 changes: 71 additions & 10 deletions camel/toolkits/function_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,30 +382,91 @@ def __init__(
f"{self.func.__name__}."
)

def _success_output(self, output: Any) -> Dict[str, Any]:
r"""Creates a successful toolkit output.

Args:
output (Any): The successful result of the tool call.

Returns:
Dict[str, Any]: A standardized success output dictionary.
"""
return {
"status": "success",
"tool_call_output": output
}

def _fail_output(self, error_message: str) -> Dict[str, Any]:
r"""Creates a failed toolkit output.

Args:
error_message (str): The error message explaining the failure.

Returns:
Dict[str, Any]: A standardized failure output dictionary.
"""
return {
"status": "fail",
"tool_call_output": "tool call failed",
"error_message": error_message
}

def __call__(self, *args: Any, **kwargs: Any) -> Any:
if self.synthesize_output:
result = self.synthesize_execution_output(args, kwargs)
return result
try:
result = self.synthesize_execution_output(args, kwargs)
return self._success_output(result)
except Exception as e:
error_msg = (
f"Execution of synthetic function {self.func.__name__} "
f"failed with arguments {args} and {kwargs}. "
f"Error: {e}"
)
logger.error(error_msg)
return self._fail_output(error_msg)
else:
# Pass the extracted arguments to the indicated function
try:
result = self.func(*args, **kwargs)
return result
return self._success_output(result)
except Exception as e:
raise ValueError(
error_msg = (
f"Execution of function {self.func.__name__} failed with "
f"arguments {args} and {kwargs}. "
f"Error: {e}"
)
logger.error(error_msg)
return self._fail_output(error_msg)

async def async_call(self, *args: Any, **kwargs: Any) -> Any:
if self.synthesize_output:
result = self.synthesize_execution_output(args, kwargs)
return result
if self.is_async:
return await self.func(*args, **kwargs)
try:
result = self.synthesize_execution_output(args, kwargs)
return self._success_output(result)
except Exception as e:
error_msg = (
f"Async execution of synthetic function "
f"{self.func.__name__} failed with arguments "
f"{args} and {kwargs}. Error: {e}"
)
logger.error(error_msg)
return self._fail_output(error_msg)
else:
return self.func(*args, **kwargs)
try:
if inspect.iscoroutinefunction(self.func):
result = await self.func(*args, **kwargs)
return self._success_output(result)
else:
result = self.func(*args, **kwargs)
return self._success_output(result)
except Exception as e:
error_msg = (
f"Async execution of function {self.func.__name__} "
f"failed with arguments {args} and {kwargs}. "
f"Error: {e}"
)
logger.error(error_msg)
return self._fail_output(error_msg)

@property
def is_async(self) -> bool:
Expand Down Expand Up @@ -781,4 +842,4 @@ def parameters(self, value: Dict[str, Any]) -> None:
JSONValidator.check_schema(value)
except SchemaError as e:
raise e
self.openai_tool_schema["function"]["parameters"]["properties"] = value
self.openai_tool_schema["function"]["parameters"]["properties"] = value
94 changes: 94 additions & 0 deletions examples/toolkits/tool_call_output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========


# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========


from camel.agents import ChatAgent
from camel.models import ModelFactory
from camel.types import ModelPlatformType
from camel.toolkits import FunctionTool
from camel.configs import ChatGPTConfig
from camel.types import ModelType

def add(a: float, b: float) -> float:
r"""Adds two numbers.

Args:
a (float): The first number to be added.
b (float): The second number to be added.

Returns:
float: The sum of the two numbers.
"""
raise Exception("Deliberately set errors to simulate tool call failures")


def multiply(a: float, b: float, decimal_places: int = 2) -> float:
r"""Multiplies two numbers.

Args:
a (float): The multiplier in the multiplication.
b (float): The multiplicand in the multiplication.
decimal_places (int, optional): The number of decimal
places to round to. Defaults to 2.

Returns:
float: The product of the two numbers.
"""
return round(a * b, decimal_places)

add_tool = FunctionTool(add)
multiply_tool = FunctionTool(multiply)

sys_msg = "You are a math master and are good at all kinds of math problems."

toollist = [add_tool, multiply_tool]

model_config_dict = ChatGPTConfig(temperature=0.0).as_dict()
model = ModelFactory.create(
model_platform=ModelPlatformType.DEFAULT,
model_type=ModelType.DEFAULT,
model_config_dict=model_config_dict,
)
agent = ChatAgent(
system_message=sys_msg,
model=model,tools=toollist,
)

usr_msg = "What is the square of the sum of 19987 and 2133??"

response = agent.step(usr_msg)

print(response.msg.content)
print(response.info['tool_calls'])
"""
===============================================================================
2025-03-23 12:22:43,919 - camel.toolkits.function_tool - ERROR - Execution of
function add failed with arguments () and {'a': 19987, 'b': 2133}.
Error: Deliberately set errors to simulate tool call failures
...
2025-03-23 12:22:43,919 - root - ERROR - Tool 'add' failed after 3 attempts.
Final error: Execution of function add failed with arguments ()
and {'a': 19987, 'b': 2133}.
Error: Deliberately set errors to simulate tool call failures

19987 plus 2133 equals 22120. The square of 22120 is 489,294,400.
[ToolCallingRecord(tool_name='add', args={'a': 19987, 'b': 2133},
result='tool call failed', tool_call_id='call_db0bc43bf0f74a6fb5767b'),
ToolCallingRecord(tool_name='multiply',
args={'a': 22120, 'b': 22120, 'decimal_places': 2},
result=489294400, tool_call_id='call_c1577a0313d542789f54c3')]
===============================================================================
"""
Loading