diff --git a/src/crewai/agents/crew_agent_executor.py b/src/crewai/agents/crew_agent_executor.py index 813ac8a08..1377482ad 100644 --- a/src/crewai/agents/crew_agent_executor.py +++ b/src/crewai/agents/crew_agent_executor.py @@ -130,6 +130,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): try: self._format_answer(answer) except OutputParserException as e: + print("ERROR ATTEMPTING TO PARSE ANSWER: ", answer) if ( FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE in e.error @@ -147,7 +148,10 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): # Directly append the result to the messages if the # tool is "Add image to content" in case of multimodal # agents - if formatted_answer.tool == self._i18n.tools("add_image")["name"]: + if ( + formatted_answer.tool + == self._i18n.tools("add_image")["name"] + ): self.messages.append(tool_result.result) continue @@ -155,7 +159,9 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): if self.step_callback: self.step_callback(tool_result) - formatted_answer.text += f"\nObservation: {tool_result.result}" + formatted_answer.text += ( + f"\nObservation: {tool_result.result}" + ) formatted_answer.result = tool_result.result if tool_result.result_as_answer: @@ -272,7 +278,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): agent=self.agent, action=agent_action, ) - tool_calling = tool_usage.parse(agent_action.text) + tool_calling = tool_usage.parse_tool_calling(agent_action.text) if isinstance(tool_calling, ToolUsageErrorException): tool_result = tool_calling.message diff --git a/src/crewai/tools/tool_usage.py b/src/crewai/tools/tool_usage.py index 3ba48222c..eddecaa38 100644 --- a/src/crewai/tools/tool_usage.py +++ b/src/crewai/tools/tool_usage.py @@ -1,9 +1,12 @@ import ast import datetime +import json import time from difflib import SequenceMatcher from textwrap import dedent -from typing import Any, List, Union +from typing import Any, Dict, List, Union + +from json_repair import repair_json import crewai.utilities.events as events from crewai.agents.tools_handler import ToolsHandler @@ -19,7 +22,15 @@ try: import agentops # type: ignore except ImportError: agentops = None -OPENAI_BIGGER_MODELS = ["gpt-4", "gpt-4o", "o1-preview", "o1-mini", "o1", "o3", "o3-mini"] +OPENAI_BIGGER_MODELS = [ + "gpt-4", + "gpt-4o", + "o1-preview", + "o1-mini", + "o1", + "o3", + "o3-mini", +] class ToolUsageErrorException(Exception): @@ -80,7 +91,7 @@ class ToolUsage: self._max_parsing_attempts = 2 self._remember_format_after_usages = 4 - def parse(self, tool_string: str): + def parse_tool_calling(self, tool_string: str): """Parse the tool string and return the tool calling.""" return self._tool_calling(tool_string) @@ -94,7 +105,6 @@ class ToolUsage: self.task.increment_tools_errors() return error - # BUG? The code below seems to be unreachable try: tool = self._select_tool(calling.tool_name) except Exception as e: @@ -104,19 +114,7 @@ class ToolUsage: self._printer.print(content=f"\n\n{error}\n", color="red") return error - if isinstance(tool, CrewStructuredTool) and tool.name == self._i18n.tools("add_image")["name"]: # type: ignore - try: - result = self._use(tool_string=tool_string, tool=tool, calling=calling) - return result - - except Exception as e: - error = getattr(e, "message", str(e)) - self.task.increment_tools_errors() - if self.agent.verbose: - self._printer.print(content=f"\n\n{error}\n", color="red") - return error - - return f"{self._use(tool_string=tool_string, tool=tool, calling=calling)}" # type: ignore # BUG?: "_use" of "ToolUsage" does not return a value (it only ever returns None) + return f"{self._use(tool_string=tool_string, tool=tool, calling=calling)}" def _use( self, @@ -349,13 +347,14 @@ class ToolUsage: tool_name = self.action.tool tool = self._select_tool(tool_name) try: - tool_input = self._validate_tool_input(self.action.tool_input) - arguments = ast.literal_eval(tool_input) + arguments = self._validate_tool_input(self.action.tool_input) + print("Arguments:", arguments) + print("Arguments type:", type(arguments)) except Exception: if raise_error: raise else: - return ToolUsageErrorException( # type: ignore # Incompatible return value type (got "ToolUsageErrorException", expected "ToolCalling | InstructorToolCalling") + return ToolUsageErrorException( f'{self._i18n.errors("tool_arguments_error")}' ) @@ -363,14 +362,14 @@ class ToolUsage: if raise_error: raise else: - return ToolUsageErrorException( # type: ignore # Incompatible return value type (got "ToolUsageErrorException", expected "ToolCalling | InstructorToolCalling") + return ToolUsageErrorException( f'{self._i18n.errors("tool_arguments_error")}' ) return ToolCalling( tool_name=tool.name, arguments=arguments, - log=tool_string, # type: ignore + log=tool_string, ) def _tool_calling( @@ -396,57 +395,26 @@ class ToolUsage: ) return self._tool_calling(tool_string) - def _validate_tool_input(self, tool_input: str) -> str: + def _validate_tool_input(self, tool_input: str) -> Dict[str, Any]: + print("tool_input:", tool_input) try: - ast.literal_eval(tool_input) - return tool_input - except Exception: - # Clean and ensure the string is properly enclosed in braces - tool_input = tool_input.strip() - if not tool_input.startswith("{"): - tool_input = "{" + tool_input - if not tool_input.endswith("}"): - tool_input += "}" + # Try to parse with json.loads directly + arguments = json.loads(tool_input) + return arguments + except json.JSONDecodeError: + # Fix common issues in the tool_input string - # Manually split the input into key-value pairs - entries = tool_input.strip("{} ").split(",") - formatted_entries = [] + # Replace single quotes with double quotes + tool_input = tool_input.replace("'", '"') - for entry in entries: - if ":" not in entry: - continue # Skip malformed entries - key, value = entry.split(":", 1) - - # Remove extraneous white spaces and quotes, replace single quotes - key = key.strip().strip('"').replace("'", '"') - value = value.strip() - - # Handle replacement of single quotes at the start and end of the value string - if value.startswith("'") and value.endswith("'"): - value = value[1:-1] # Remove single quotes - value = ( - '"' + value.replace('"', '\\"') + '"' - ) # Re-encapsulate with double quotes - elif value.isdigit(): # Check if value is a digit, hence integer - value = value - elif value.lower() in [ - "true", - "false", - ]: # Check for boolean and null values - value = value.lower().capitalize() - elif value.lower() == "null": - value = "None" - else: - # Assume the value is a string and needs quotes - value = '"' + value.replace('"', '\\"') + '"' - - # Rebuild the entry with proper quoting - formatted_entry = f'"{key}": {value}' - formatted_entries.append(formatted_entry) - - # Reconstruct the JSON string - new_json_string = "{" + ", ".join(formatted_entries) + "}" - return new_json_string + # Use json_repair to fix common JSON issues + repaired_input = repair_json(tool_input) + try: + arguments = json.loads(repaired_input) + return arguments + except json.JSONDecodeError as e: + # If all else fails, raise an error + raise Exception(f"Invalid tool input JSON: {e}") def on_tool_error(self, tool: Any, tool_calling: ToolCalling, e: Exception) -> None: event_data = self._prepare_event_data(tool, tool_calling)