Skip to content

Commit

Permalink
Feature/use converter instead of manually trimming (#894)
Browse files Browse the repository at this point in the history
* Exploring output being passed to tool selector to see if we can better format data

* WIP. Adding JSON repair functionality

* Almost done implementing JSON repair. Testing fixes vs current base case.

* More action cleanup with additional tests

* WIP. Trying to figure out what is going on with tool descriptions

* Update tool description generation

* WIP. Trying to find out what is causing the tools to duplicate

* Replacing tools properly instead of duplicating them accidentally

* Fixing issues for MR

* Update dependencies for JSON_REPAIR

* More cleaning up pull request

* preppering for call

* Fix type-checking issues

---------

Co-authored-by: João Moura <joaomdmoura@gmail.com>
  • Loading branch information
bhancockio and joaomdmoura committed Jul 15, 2024
1 parent 4eb4073 commit 7acf0b2
Show file tree
Hide file tree
Showing 13 changed files with 552 additions and 68 deletions.
23 changes: 17 additions & 6 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ appdirs = "^1.4.4"
jsonref = "^1.1.0"
agentops = { version = "^0.1.9", optional = true }
embedchain = "^0.1.114"
json-repair = "^0.25.2"

[tool.poetry.extras]
tools = ["crewai-tools"]
Expand Down
63 changes: 57 additions & 6 deletions src/crewai/agent.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import os
from inspect import signature
from typing import Any, List, Optional, Tuple

from langchain.agents.agent import RunnableAgent
from langchain.agents.tools import BaseTool
from langchain.agents.tools import tool as LangChainTool
from langchain.tools.render import render_text_description
from langchain_core.agents import AgentAction
from langchain_core.callbacks import BaseCallbackHandler
from langchain_openai import ChatOpenAI
Expand Down Expand Up @@ -167,14 +168,16 @@ def execute_task(
if memory.strip() != "":
task_prompt += self.i18n.slice("memory").format(memory=memory)

tools = tools or self.tools

parsed_tools = self._parse_tools(tools or []) # type: ignore # Argument 1 to "_parse_tools" of "Agent" has incompatible type "list[Any] | None"; expected "list[Any]"
tools = tools or self.tools or []
parsed_tools = self._parse_tools(tools)
self.create_agent_executor(tools=tools)
self.agent_executor.tools = parsed_tools
self.agent_executor.task = task

self.agent_executor.tools_description = render_text_description(parsed_tools)
# TODO: COMPARE WITH ARGS AND WITHOUT ARGS
self.agent_executor.tools_description = self._render_text_description_and_args(
parsed_tools
)
self.agent_executor.tools_names = self.__tools_names(parsed_tools)

if self.crew and self.crew._train:
Expand All @@ -189,6 +192,7 @@ def execute_task(
"tools": self.agent_executor.tools_description,
}
)["output"]

if self.max_rpm:
self._rpm_controller.stop_rpm_counter()

Expand Down Expand Up @@ -220,7 +224,7 @@ def create_agent_executor(self, tools=None) -> None:
Returns:
An instance of the CrewAgentExecutor class.
"""
tools = tools or self.tools
tools = tools or self.tools or []

agent_args = {
"input": lambda x: x["input"],
Expand Down Expand Up @@ -315,6 +319,7 @@ def _parse_tools(self, tools: List[Any]) -> List[LangChainTool]: # type: ignore
tools_list = []
for tool in tools:
tools_list.append(tool)

return tools_list

def _training_handler(self, task_prompt: str) -> str:
Expand All @@ -341,6 +346,52 @@ def _use_trained_data(self, task_prompt: str) -> str:
)
return task_prompt

def _render_text_description(self, tools: List[BaseTool]) -> str:
"""Render the tool name and description in plain text.
Output will be in the format of:
.. code-block:: markdown
search: This tool is used for search
calculator: This tool is used for math
"""
description = "\n".join(
[
f"Tool name: {tool.name}\nTool description:\n{tool.description}"
for tool in tools
]
)

return description

def _render_text_description_and_args(self, tools: List[BaseTool]) -> str:
"""Render the tool name, description, and args in plain text.
Output will be in the format of:
.. code-block:: markdown
search: This tool is used for search, args: {"query": {"type": "string"}}
calculator: This tool is used for math, \
args: {"expression": {"type": "string"}}
"""
tool_strings = []
for tool in tools:
args_schema = str(tool.args)
if hasattr(tool, "func") and tool.func:
sig = signature(tool.func)
description = (
f"Tool Name: {tool.name}{sig}\nTool Description: {tool.description}"
)
else:
description = (
f"Tool Name: {tool.name}\nTool Description: {tool.description}"
)
tool_strings.append(f"{description}\nTool Arguments: {args_schema}")

return "\n".join(tool_strings)

@staticmethod
def __tools_names(tools) -> str:
return ", ".join([t.name for t in tools])
Expand Down
15 changes: 9 additions & 6 deletions src/crewai/agents/agent_builder/utilities/base_agent_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def _get_coworker(self, coworker: Optional[str], **kwargs) -> Optional[str]:
is_list = coworker.startswith("[") and coworker.endswith("]")
if is_list:
coworker = coworker[1:-1].split(",")[0]

return coworker

def delegate_work(
Expand All @@ -40,11 +41,13 @@ def ask_question(
coworker = self._get_coworker(coworker, **kwargs)
return self._execute(coworker, question, context)

def _execute(self, agent: Union[str, None], task: str, context: Union[str, None]):
def _execute(
self, agent_name: Union[str, None], task: str, context: Union[str, None]
):
"""Execute the command."""
try:
if agent is None:
agent = ""
if agent_name is None:
agent_name = ""

# It is important to remove the quotes from the agent name.
# The reason we have to do this is because less-powerful LLM's
Expand All @@ -53,7 +56,7 @@ def _execute(self, agent: Union[str, None], task: str, context: Union[str, None]
# {"task": "....", "coworker": "....
# when it should look like this:
# {"task": "....", "coworker": "...."}
agent_name = agent.casefold().replace('"', "").replace("\n", "")
agent_name = agent_name.casefold().replace('"', "").replace("\n", "")

agent = [ # type: ignore # Incompatible types in assignment (expression has type "list[BaseAgent]", variable has type "str | None")
available_agent
Expand All @@ -75,9 +78,9 @@ def _execute(self, agent: Union[str, None], task: str, context: Union[str, None]
)

agent = agent[0]
task = Task( # type: ignore # Incompatible types in assignment (expression has type "Task", variable has type "str")
task_with_assigned_agent = Task( # type: ignore # Incompatible types in assignment (expression has type "Task", variable has type "str")
description=task,
agent=agent,
expected_output="Your best answer to your coworker asking you this, accounting for the context shared.",
)
return agent.execute_task(task, context) # type: ignore # "str" has no attribute "execute_task"
return agent.execute_task(task_with_assigned_agent, context)
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from abc import ABC, abstractmethod
from typing import Any, Optional

from pydantic import BaseModel, Field, PrivateAttr
from pydantic import BaseModel, Field


class OutputConverter(BaseModel, ABC):
Expand All @@ -21,7 +21,6 @@ class OutputConverter(BaseModel, ABC):
max_attempts (int): Maximum number of conversion attempts (default: 3).
"""

_is_gpt: bool = PrivateAttr(default=True)
text: str = Field(description="Text to be converted.")
llm: Any = Field(description="The language model to be used to convert the text.")
model: Any = Field(description="The model to be used to convert the text.")
Expand All @@ -41,7 +40,8 @@ def to_json(self, current_attempt=1):
"""Convert text to json."""
pass

@abstractmethod # type: ignore # Name "_is_gpt" already defined on line 25
def _is_gpt(self, llm): # type: ignore # Name "_is_gpt" already defined on line 25
@property
@abstractmethod
def is_gpt(self) -> bool:
"""Return if llm provided is of gpt from openai."""
pass
14 changes: 2 additions & 12 deletions src/crewai/agents/executor.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
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
Expand All @@ -19,9 +11,7 @@
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
Expand Down
42 changes: 37 additions & 5 deletions src/crewai/agents/parser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re
from typing import Any, Union

from json_repair import repair_json
from langchain.agents.output_parsers import ReActSingleInputOutputParser
from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.exceptions import OutputParserException
Expand Down Expand Up @@ -48,11 +49,15 @@ def parse(self, text: str) -> Union[AgentAction, AgentFinish]:
raise OutputParserException(
f"{FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE}: {text}"
)
action = action_match.group(1).strip()
action_input = action_match.group(2)
tool_input = action_input.strip(" ")
tool_input = tool_input.strip('"')
return AgentAction(action, tool_input, text)
action = action_match.group(1)
clean_action = self._clean_action(action)

action_input = action_match.group(2).strip()

tool_input = action_input.strip(" ").strip('"')
safe_tool_input = self._safe_repair_json(tool_input)

return AgentAction(clean_action, safe_tool_input, text)

elif includes_answer:
return AgentFinish(
Expand Down Expand Up @@ -87,3 +92,30 @@ def parse(self, text: str) -> Union[AgentAction, AgentFinish]:
llm_output=text,
send_to_llm=True,
)

def _clean_action(self, text: str) -> str:
"""Clean action string by removing non-essential formatting characters."""
return re.sub(r"^\s*\*+\s*|\s*\*+\s*$", "", text).strip()

def _safe_repair_json(self, tool_input: str) -> str:
UNABLE_TO_REPAIR_JSON_RESULTS = ['""', "{}"]

# Skip repair if the input starts and ends with square brackets
# Explanation: The JSON parser has issues handling inputs that are enclosed in square brackets ('[]').
# These are typically valid JSON arrays or strings that do not require repair. Attempting to repair such inputs
# might lead to unintended alterations, such as wrapping the entire input in additional layers or modifying
# the structure in a way that changes its meaning. By skipping the repair for inputs that start and end with
# square brackets, we preserve the integrity of these valid JSON structures and avoid unnecessary modifications.
if tool_input.startswith("[") and tool_input.endswith("]"):
return tool_input

# Before repair, handle common LLM issues:
# 1. Replace """ with " to avoid JSON parser errors

tool_input = tool_input.replace('"""', '"')

result = repair_json(tool_input)
if result in UNABLE_TO_REPAIR_JSON_RESULTS:
return tool_input

return str(result)
Loading

0 comments on commit 7acf0b2

Please sign in to comment.