Improving tool calling to pass dictionaries instead of strings

This commit is contained in:
Brandon Hancock
2025-01-08 15:22:55 -05:00
parent b3504e768c
commit 2107512e84
2 changed files with 47 additions and 73 deletions

View File

@@ -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

View File

@@ -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)