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

Feature/use converter instead of manually trimming #894

Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
WIP. Trying to figure out what is going on with tool descriptions
  • Loading branch information
bhancockio committed Jul 5, 2024
commit 698eb2afc08abeedeb9996335c77e3bbcb3a3419
11 changes: 11 additions & 0 deletions src/crewai/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ def execute_task(
Returns:
Output of the agent
"""
print("EXECUTE TASK AGENT:", self.role)
if self.tools_handler:
# type: ignore # Incompatible types in assignment (expression has type "dict[Never, Never]", variable has type "ToolCalling")
self.tools_handler.last_used_tool = {}
Expand Down Expand Up @@ -176,6 +177,13 @@ def execute_task(
self.agent_executor.tools_description = render_text_description(parsed_tools)
self.agent_executor.tools_names = self.__tools_names(parsed_tools)

print("AGENT AGENT EXECUTOR TOOLS:", self.agent_executor.tools)
print(
"AGENT AGENT EXECUTOR TOOLS DESCRIPTION:",
self.agent_executor.tools_description,
)
print("AGENT AGENT EXECUTOR TOOLS NAMES:", self.agent_executor.tools_names)

if self.crew and self.crew._train:
task_prompt = self._training_handler(task_prompt=task_prompt)
else:
Expand Down Expand Up @@ -274,6 +282,8 @@ def create_agent_executor(self, tools=None) -> None:
agent=RunnableAgent(runnable=inner_agent), **executor_args
)

# print("AGENT EXECUTOR REVIEW:", self.agent_executor.to)

def get_delegation_tools(self, agents: List[BaseAgent]):
agent_tools = AgentTools(agents=agents)
tools = agent_tools.tools()
Expand Down Expand Up @@ -308,6 +318,7 @@ def _parse_tools(self, tools: List[Any]) -> List[LangChainTool]:
tools_list = []
for tool in tools:
tools_list.append(tool)

return tools_list

def _training_handler(self, task_prompt: str) -> str:
Expand Down
30 changes: 16 additions & 14 deletions src/crewai/agents/executor.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,22 @@
import threading
import time
from typing import (
Any,
Dict,
Iterator,
List,
Optional,
Tuple,
Union,
)
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union

from langchain.agents import AgentExecutor
from langchain.agents.agent import ExceptionTool
from langchain.callbacks.manager import CallbackManagerForChainRun
from langchain_core.agents import AgentAction, AgentFinish, AgentStep
from langchain_core.exceptions import OutputParserException

from langchain_core.tools import BaseTool
from langchain_core.utils.input import get_color_mapping
from pydantic import InstanceOf
from crewai.agents.agent_builder.base_agent_executor_mixin import (
CrewAgentExecutorMixin,
)

from crewai.agents.agent_builder.base_agent_executor_mixin import CrewAgentExecutorMixin
from crewai.agents.tools_handler import ToolsHandler
from crewai.tools.tool_usage import ToolUsage, ToolUsageErrorException
from crewai.utilities import I18N
from crewai.utilities.constants import TRAINING_DATA_FILE
from crewai.utilities.training_handler import CrewTrainingHandler
from crewai.utilities import I18N


class CrewAgentExecutor(AgentExecutor, CrewAgentExecutorMixin):
Expand Down Expand Up @@ -131,6 +120,9 @@ def _iter_next_step(

Override this to take control of how the agent makes and acts on choices.
"""
print("TOOLS DESCRIPTION IN CREWAGENTEXECUTOR: ", self.tools_description)
print("TOOLS NAMES IN CREWAGENTEXECUTOR: ", self.tools_names)

try:
if self._should_force_answer():
error = self._i18n.errors("force_final_answer")
Expand Down Expand Up @@ -245,6 +237,16 @@ def _iter_next_step(
task=self.task,
action=agent_action,
)
# print("TOOL USAGE CALLED IN CREWAGENTEXECUTOR: ", tool_usage)
# print(
# "TOOL USAGE CALLED IN CREWAGENTEXECUTOR tool descriptions: ",
# tool_usage.tools_description,
# )
# print(
# "TOOL USAGE CALLED IN CREWAGENTEXECUTOR tool names: ",
# tool_usage.tools_names,
# )
# print("TOOL USAGE PARSEL CALLED: ", agent_action.log)
tool_calling = tool_usage.parse(agent_action.log)

if isinstance(tool_calling, ToolUsageErrorException):
Expand Down
2 changes: 1 addition & 1 deletion src/crewai/tools/agent_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class AgentTools(BaseAgentTools):
"""Default tools around agent delegation"""

def tools(self):
coworkers = f"[{', '.join([f'{agent.role}' for agent in self.agents])}]"
coworkers = ", ".join([f"{agent.role}" for agent in self.agents])
tools = [
StructuredTool.from_function(
func=self.delegate_work,
Expand Down
24 changes: 13 additions & 11 deletions src/crewai/tools/tool_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,16 @@ def use(
self.task.increment_tools_errors()
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)}" # type: ignore # BUG?: "_use" of "ToolUsage" does not return a value (it only ever returns None)

def _use(
self,
tool_string: str,
tool: BaseTool,
calling: Union[ToolCalling, InstructorToolCalling],
) -> str: # TODO: Fix this return type
tool_event = agentops.ToolEvent(name=calling.tool_name) if agentops else None
if self._check_tool_repeated_usage(calling=calling): # type: ignore # _check_tool_repeated_usage of "ToolUsage" does not return a value (it only ever returns None)
tool_event = agentops.ToolEvent(name=calling.tool_name) if agentops else None
if self._check_tool_repeated_usage(calling=calling): # type: ignore # _check_tool_repeated_usage of "ToolUsage" does not return a value (it only ever returns None)
try:
result = self._i18n.errors("task_repeated_usage").format(
tool_names=self.tools_names
Expand All @@ -117,20 +117,20 @@ def _use(
tool_name=tool.name,
attempts=self._run_attempts,
)
result = self._format_result(result=result) # type: ignore # "_format_result" of "ToolUsage" does not return a value (it only ever returns None)
result = self._format_result(result=result) # type: ignore # "_format_result" of "ToolUsage" does not return a value (it only ever returns None)
return result # type: ignore # Fix the reutrn type of this function

except Exception:
self.task.increment_tools_errors()

result = None # type: ignore # Incompatible types in assignment (expression has type "None", variable has type "str")
result = None # type: ignore # Incompatible types in assignment (expression has type "None", variable has type "str")

if self.tools_handler.cache:
result = self.tools_handler.cache.read( # type: ignore # Incompatible types in assignment (expression has type "str | None", variable has type "str")
tool=calling.tool_name, input=calling.arguments
)

if result is None: #! finecwg: if not result --> if result is None
if result is None: #! finecwg: if not result --> if result is None
try:
if calling.tool_name in [
"Delegate work to coworker",
Expand All @@ -140,7 +140,7 @@ def _use(

if calling.arguments:
try:
acceptable_args = tool.args_schema.schema()["properties"].keys() # type: ignore # Item "None" of "type[BaseModel] | None" has no attribute "schema"
acceptable_args = tool.args_schema.schema()["properties"].keys() # type: ignore # Item "None" of "type[BaseModel] | None" has no attribute "schema"
arguments = {
k: v
for k, v in calling.arguments.items()
Expand All @@ -152,7 +152,7 @@ def _use(
arguments = calling.arguments
result = tool._run(**arguments)
else:
arguments = calling.arguments.values() # type: ignore # Incompatible types in assignment (expression has type "dict_values[str, Any]", variable has type "dict[str, Any]")
arguments = calling.arguments.values() # type: ignore # Incompatible types in assignment (expression has type "dict_values[str, Any]", variable has type "dict[str, Any]")
result = tool._run(*arguments)
else:
result = tool._run()
Expand Down Expand Up @@ -201,14 +201,14 @@ def _use(
llm=self.function_calling_llm,
tool_name=tool.name,
attempts=self._run_attempts,
)
result = self._format_result(result=result) # type: ignore # "_format_result" of "ToolUsage" does not return a value (it only ever returns None)
)
result = self._format_result(result=result) # type: ignore # "_format_result" of "ToolUsage" does not return a value (it only ever returns None)
return result # type: ignore # No return value expected

def _format_result(self, result: Any) -> None:
self.task.used_tools += 1
if self._should_remember_format(): # type: ignore # "_should_remember_format" of "ToolUsage" does not return a value (it only ever returns None)
result = self._remember_format(result=result) # type: ignore # "_remember_format" of "ToolUsage" does not return a value (it only ever returns None)
result = self._remember_format(result=result) # type: ignore # "_remember_format" of "ToolUsage" does not return a value (it only ever returns None)
return result

def _should_remember_format(self) -> None:
Expand Down Expand Up @@ -249,6 +249,7 @@ def _select_tool(self, tool_name: str) -> BaseTool:
):
return tool
self.task.increment_tools_errors()
# TODO: IMPROVE THIS ERROR MESSAGE BECAUSE IT'S CONFUSING THE LLM
if tool_name and tool_name != "":
raise Exception(
f"Action '{tool_name}' don't exist, these are the only available Actions:\n {self.tools_description}"
Expand Down Expand Up @@ -347,6 +348,7 @@ def _validate_tool_input(self, tool_input: str) -> str:
return tool_input
except Exception:
# Clean and ensure the string is properly enclosed in braces
# TODO: MAKE SURE THE INPUT IS A STRING AND NOT A LIST
tool_input = tool_input.strip()
if not tool_input.startswith("{"):
tool_input = "{" + tool_input
Expand Down
109 changes: 105 additions & 4 deletions tests/agent_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
from unittest.mock import patch

import pytest
from langchain.tools import tool
from langchain.tools import StructuredTool, tool
from langchain_core.exceptions import OutputParserException
from langchain_openai import ChatOpenAI
from pydantic import BaseModel

from crewai import Agent, Crew, Task
from crewai.agents.cache import CacheHandler
from crewai.agents.executor import CrewAgentExecutor
from crewai.agents.parser import CrewAgentParser

from crewai.tools.tool_calling import InstructorToolCalling
from crewai.tools.tool_usage import ToolUsage
from crewai.utilities import RPMController
Expand Down Expand Up @@ -645,7 +645,7 @@ def callback(self, step):
with patch.object(StepCallback, "callback") as callback:

@tool
def learn_about_AI(topic) -> float:
def learn_about_AI(topic) -> str:
"""Useful for when you need to learn about AI to write an paragraph about it."""
return "AI is a very broad field."

Expand Down Expand Up @@ -679,7 +679,7 @@ def test_agent_function_calling_llm():
with patch.object(llm.client, "create", wraps=llm.client.create) as private_mock:

@tool
def learn_about_AI(topic) -> float:
def learn_about_AI(topic) -> str:
"""Useful for when you need to learn about AI to write an paragraph about it."""
return "AI is a very broad field."

Expand Down Expand Up @@ -895,3 +895,104 @@ def test_agent_use_trained_data(crew_training_handler):
crew_training_handler.assert_has_calls(
[mock.call(), mock.call("trained_agents_data.pkl"), mock.call().load()]
)


# Mock class to simulate CrewAITool
class MockCrewAITool:
def to_langchain(self):
return StructuredTool(name="mock_tool", func=lambda x: x)


# Mock class to simulate non-tool objects
class NonTool:
pass


def test_parse_tools_with_crewai_tool():
agent = Agent(role="test role", goal="test goal", backstory="test backstory")

tools = [MockCrewAITool()]
parsed_tools = agent._parse_tools(tools)

assert len(parsed_tools) == 1
assert isinstance(parsed_tools[0], StructuredTool)
assert parsed_tools[0].name == "mock_tool"


# def test_parse_tools_with_regular_tool():
# # Using an existing StructuredTool from langchain.tools
# def mock_tool_function(x):
# return x

# mock_tool = StructuredTool(
# name="mock_tool", func=mock_tool_function, args_schema=None
# )

# agent = Agent(role="test role", goal="test goal", backstory="test backstory")

# tools = [mock_tool]
# parsed_tools = agent._parse_tools(tools)

# assert len(parsed_tools) == 1
# assert parsed_tools[0] == mock_tool


# def test_parse_tools_with_mixed_tools():
# # Using an existing StructuredTool from langchain.tools
# def mock_tool_function(x):
# return x

# mock_tool = StructuredTool(name="regular_tool", func=mock_tool_function)

# agent = Agent(role="test role", goal="test goal", backstory="test backstory")

# tools = [MockCrewAITool(), mock_tool]
# parsed_tools = agent._parse_tools(tools)

# assert len(parsed_tools) == 2
# assert isinstance(parsed_tools[0], StructuredTool)
# assert parsed_tools[0].name == "mock_tool"
# assert parsed_tools[1] == mock_tool


# def test_parse_tools_with_no_tools():
# agent = Agent(role="test role", goal="test goal", backstory="test backstory")

# tools = []
# parsed_tools = agent._parse_tools(tools)

# assert len(parsed_tools) == 0


# def test_parse_tools_with_non_tool_objects():
# agent = Agent(role="test role", goal="test goal", backstory="test backstory")

# tools = [NonTool()]
# parsed_tools = agent._parse_tools(tools)

# assert len(parsed_tools) == 1
# assert isinstance(parsed_tools[0], NonTool)


# @pytest.mark.parametrize(
# "tool_class_name, expected_instance",
# [("MockCrewAITool", StructuredTool), ("NonTool", NonTool)],
# )
# def test_parse_tools_dynamic_import(tool_class_name, expected_instance):
# agent = Agent(role="test role", goal="test goal", backstory="test backstory")

# # Dynamically create tool class
# ToolClass = type(
# tool_class_name,
# (),
# {
# "to_langchain": lambda self: StructuredTool(
# name="dynamic_tool", func=lambda x: x
# )
# },
# )
# tools = [ToolClass()]
# parsed_tools = agent._parse_tools(tools)

# assert len(parsed_tools) == 1
# assert isinstance(parsed_tools[0], expected_instance)
16 changes: 14 additions & 2 deletions tests/agents/test_crew_agent_parser.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import pytest
from crewai.agents.parser import CrewAgentParser
from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.exceptions import OutputParserException

from crewai.agents.parser import CrewAgentParser


@pytest.fixture
def parser():
Expand All @@ -20,6 +19,19 @@ def test_valid_action_parsing_special_characters(parser):
assert result.tool_input == "what's the temperature in SF?"


def test_valid_action_parsing_with_json_tool_input(parser):
text = """
Thought: Let's find the information
Action: query
Action Input: ** {"task": "What are some common challenges or barriers that you have observed or experienced when implementing AI-powered solutions in healthcare settings?", "context": "As we've discussed recent advancements in AI applications in healthcare, it's crucial to acknowledge the potential hurdles. Some possible obstacles include...", "coworker": "Senior Researcher"}
"""
result = parser.parse(text)
assert isinstance(result, AgentAction)
expected_tool_input = '{"task": "What are some common challenges or barriers that you have observed or experienced when implementing AI-powered solutions in healthcare settings?", "context": "As we\'ve discussed recent advancements in AI applications in healthcare, it\'s crucial to acknowledge the potential hurdles. Some possible obstacles include...", "coworker": "Senior Researcher"}'
assert result.tool == "query"
assert result.tool_input == expected_tool_input


def test_valid_action_parsing_with_quotes(parser):
text = 'Thought: Let\'s find the temperature\nAction: search\nAction Input: "temperature in SF"'
result = parser.parse(text)
Expand Down