mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-09 16:18:30 +00:00
revamping crewai tool
This commit is contained in:
19
README.md
19
README.md
@@ -23,7 +23,6 @@ In the realm of CrewAI agents, tools are pivotal for enhancing functionality. Th
|
||||
|
||||
- [Creating Your Tools](#creating-your-tools)
|
||||
- [Subclassing `BaseTool`](#subclassing-basetool)
|
||||
- [Functional Tool Creation](#functional-tool-creation)
|
||||
- [Utilizing the `tool` Decorator](#utilizing-the-tool-decorator)
|
||||
- [Contribution Guidelines](#contribution-guidelines)
|
||||
- [Development Setup](#development-setup)
|
||||
@@ -40,32 +39,26 @@ There are three ways to create tools for crewAI agents:
|
||||
### Subclassing `BaseTool`
|
||||
|
||||
```python
|
||||
from crewai_tools import BaseTool
|
||||
|
||||
class MyCustomTool(BaseTool):
|
||||
name: str = "Name of my tool"
|
||||
description: str = "Clear description for what this tool is useful for, you agent will need this information to use it."
|
||||
|
||||
def _run(self, argument) -> str:
|
||||
def _run(self, argument: str) -> str:
|
||||
# Implementation goes here
|
||||
pass
|
||||
```
|
||||
|
||||
Define a new class inheriting from `BaseTool`, specifying `name`, `description`, and the `_run` method for operational logic.
|
||||
|
||||
### Functional Tool Creation
|
||||
|
||||
```python
|
||||
my_tool = Tool(
|
||||
name="Name of my tool"
|
||||
description="Clear description for what this tool is useful for, you agent will need this information to use it.",
|
||||
func=lambda argument: # Your function logic here
|
||||
)
|
||||
```
|
||||
|
||||
For a simpler approach, create a `Tool` object directly with the required attributes and a functional logic.
|
||||
|
||||
### Utilizing the `tool` Decorator
|
||||
|
||||
For a simpler approach, create a `Tool` object directly with the required attributes and a functional logic.
|
||||
|
||||
```python
|
||||
from crewai_tools import tool
|
||||
@tool("Name of my tool")
|
||||
def my_tool(question: str) -> str:
|
||||
"""Clear description for what this tool is useful for, you agent will need this information to use it."""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .tools.base_tool import BaseTool, Tool, as_tool, tool
|
||||
from .tools.base_tool import BaseTool, Tool, tool
|
||||
from .tools import (
|
||||
CodeDocsSearchTool,
|
||||
CSVSearchTool,
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Callable, cast, Optional, Type
|
||||
|
||||
from langchain.agents import tools as langchain_tools
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, model_validator
|
||||
from pydantic.v1 import BaseModel as V1BaseModel
|
||||
|
||||
from langchain_core.tools import StructuredTool
|
||||
|
||||
class BaseTool(BaseModel, ABC):
|
||||
name: str
|
||||
"""The unique name of the tool that clearly communicates its purpose."""
|
||||
description: str
|
||||
"""Used to tell the model how/when/why to use the tool."""
|
||||
args_schema: Optional[Type[BaseModel]] = None
|
||||
args_schema: Optional[Type[V1BaseModel]] = None
|
||||
"""The schema for the arguments that the tool accepts."""
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_args_schema(self):
|
||||
self._set_args_schema()
|
||||
return self
|
||||
|
||||
def run(
|
||||
self,
|
||||
*args: Any,
|
||||
@@ -29,14 +35,28 @@ class BaseTool(BaseModel, ABC):
|
||||
) -> Any:
|
||||
"""Here goes the actual implementation of the tool."""
|
||||
|
||||
def to_langchain(self) -> langchain_tools.Tool:
|
||||
return langchain_tools.Tool(
|
||||
def to_langchain(self) -> StructuredTool:
|
||||
self._set_args_schema()
|
||||
return StructuredTool(
|
||||
name=self.name,
|
||||
description=self.description,
|
||||
args_schema=self.args_schema,
|
||||
func=self._run,
|
||||
)
|
||||
|
||||
def _set_args_schema(self):
|
||||
if self.args_schema is None:
|
||||
class_name = f"{self.__class__.__name__}Schema"
|
||||
self.args_schema = type(
|
||||
class_name,
|
||||
(V1BaseModel,),
|
||||
{
|
||||
"__annotations__": {
|
||||
k: v for k, v in self._run.__annotations__.items() if k != 'return'
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class Tool(BaseTool):
|
||||
func: Callable
|
||||
@@ -47,8 +67,8 @@ class Tool(BaseTool):
|
||||
|
||||
|
||||
def to_langchain(
|
||||
tools: list[BaseTool | langchain_tools.BaseTool],
|
||||
) -> list[langchain_tools.BaseTool]:
|
||||
tools: list[BaseTool | StructuredTool],
|
||||
) -> list[StructuredTool]:
|
||||
return [t.to_langchain() if isinstance(t, BaseTool) else t for t in tools]
|
||||
|
||||
|
||||
@@ -62,10 +82,24 @@ def tool(*args):
|
||||
if f.__doc__ is None:
|
||||
raise ValueError("Function must have a docstring")
|
||||
|
||||
args_schema = None
|
||||
if f.__annotations__:
|
||||
class_name = "".join(tool_name.split()).title()
|
||||
args_schema = type(
|
||||
class_name,
|
||||
(V1BaseModel,),
|
||||
{
|
||||
"__annotations__": {
|
||||
k: v for k, v in f.__annotations__.items() if k != 'return'
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return Tool(
|
||||
name=tool_name,
|
||||
description=f.__doc__,
|
||||
func=f,
|
||||
args_schema=args_schema,
|
||||
)
|
||||
|
||||
return _make_tool
|
||||
@@ -74,13 +108,4 @@ def tool(*args):
|
||||
return _make_with_name(args[0].__name__)(args[0])
|
||||
if len(args) == 1 and isinstance(args[0], str):
|
||||
return _make_with_name(args[0])
|
||||
raise ValueError("Invalid arguments")
|
||||
|
||||
|
||||
def as_tool(f: Any) -> BaseTool:
|
||||
"""
|
||||
Useful for when you create a tool using the @tool decorator and want to use it as a BaseTool.
|
||||
It is a BaseTool, but type inference doesn't know that.
|
||||
"""
|
||||
assert isinstance(f, BaseTool)
|
||||
return cast(BaseTool, f)
|
||||
raise ValueError("Invalid arguments")
|
||||
@@ -29,5 +29,9 @@ class DirectoryReadTool(BaseTool):
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
directory = kwargs.get('directory', self.directory)
|
||||
return [(os.path.join(root, file).replace(directory, "").lstrip(os.path.sep)) for root, dirs, files in os.walk(directory) for file in files]
|
||||
if directory[-1] == "/":
|
||||
directory = directory[:-1]
|
||||
files_list = [f"{directory}/{(os.path.join(root, filename).replace(directory, '').lstrip(os.path.sep))}" for root, dirs, files in os.walk(directory) for filename in files]
|
||||
files = "\n- ".join(files_list)
|
||||
return f"File paths: \n-{files}"
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
from typing import Callable
|
||||
|
||||
from chromadb import Documents, EmbeddingFunction, Embeddings
|
||||
from embedchain import App
|
||||
from embedchain.config import AppConfig, ChromaDbConfig
|
||||
from embedchain.embedder.base import BaseEmbedder
|
||||
from embedchain.vectordb.chroma import ChromaDB
|
||||
|
||||
from crewai_tools.adapters.embedchain_adapter import EmbedchainAdapter
|
||||
|
||||
|
||||
class MockEmbeddingFunction(EmbeddingFunction):
|
||||
fn: Callable
|
||||
|
||||
def __init__(self, embedding_fn: Callable):
|
||||
self.fn = embedding_fn
|
||||
|
||||
def __call__(self, input: Documents) -> Embeddings:
|
||||
return self.fn(input)
|
||||
|
||||
|
||||
def test_embedchain_adapter(helpers):
|
||||
embedding_function = MockEmbeddingFunction(
|
||||
embedding_fn=helpers.get_embedding_function()
|
||||
)
|
||||
embedder = BaseEmbedder()
|
||||
embedder.set_embedding_fn(embedding_function) # type: ignore
|
||||
|
||||
db = ChromaDB(
|
||||
config=ChromaDbConfig(
|
||||
dir="tests/data/chromadb",
|
||||
collection_name="requirements",
|
||||
)
|
||||
)
|
||||
|
||||
app = App(
|
||||
config=AppConfig(
|
||||
id="test",
|
||||
),
|
||||
db=db,
|
||||
embedding_model=embedder,
|
||||
)
|
||||
|
||||
adapter = EmbedchainAdapter(
|
||||
dry_run=True,
|
||||
embedchain_app=app,
|
||||
)
|
||||
|
||||
assert (
|
||||
adapter.query("What are the requirements for the task?")
|
||||
== """
|
||||
Use the following pieces of context to answer the query at the end.
|
||||
If you don't know the answer, just say that you don't know, don't try to make up an answer.
|
||||
|
||||
Technical requirements
|
||||
|
||||
The system should be able to process 1000 transactions per second. The code must be written in Ruby. | Problem
|
||||
|
||||
Currently, we are not able to find out palindromes in a given string. We need a solution to this problem. | Solution
|
||||
|
||||
We need a function that takes a string as input and returns true if the string is a palindrome, otherwise false.
|
||||
|
||||
Query: What are the requirements for the task?
|
||||
|
||||
Helpful Answer:
|
||||
"""
|
||||
)
|
||||
@@ -1,22 +0,0 @@
|
||||
from crewai_tools.adapters.lancedb_adapter import LanceDBAdapter
|
||||
|
||||
|
||||
def test_lancedb_adapter(helpers):
|
||||
adapter = LanceDBAdapter(
|
||||
uri="tests/data/lancedb",
|
||||
table_name="requirements",
|
||||
embedding_function=helpers.get_embedding_function(),
|
||||
top_k=2,
|
||||
vector_column_name="vector",
|
||||
text_column_name="text",
|
||||
)
|
||||
|
||||
assert (
|
||||
adapter.query("What are the requirements for the task?")
|
||||
== """Technical requirements
|
||||
|
||||
The system should be able to process 1000 transactions per second. The code must be written in Ruby.
|
||||
Problem
|
||||
|
||||
Currently, we are not able to find out palindromes in a given string. We need a solution to this problem."""
|
||||
)
|
||||
46
tests/base_tool_test.py
Normal file
46
tests/base_tool_test.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import json
|
||||
import pydantic_core
|
||||
import pytest
|
||||
from crewai_tools import BaseTool, tool
|
||||
|
||||
def test_creating_a_tool_using_annotation():
|
||||
@tool("Name of my tool")
|
||||
def my_tool(question: str) -> str:
|
||||
"""Clear description for what this tool is useful for, you agent will need this information to use it."""
|
||||
return question
|
||||
|
||||
# Assert all the right attributes were defined
|
||||
assert my_tool.name == "Name of my tool"
|
||||
assert my_tool.description == "Clear description for what this tool is useful for, you agent will need this information to use it."
|
||||
assert my_tool.args_schema.schema()["properties"] == {'question': {'title': 'Question', 'type': 'string'}}
|
||||
assert my_tool.func("What is the meaning of life?") == "What is the meaning of life?"
|
||||
|
||||
# Assert the langchain tool conversion worked as expected
|
||||
converted_tool = my_tool.to_langchain()
|
||||
assert converted_tool.name == "Name of my tool"
|
||||
assert converted_tool.description == "Clear description for what this tool is useful for, you agent will need this information to use it."
|
||||
assert converted_tool.args_schema.schema()["properties"] == {'question': {'title': 'Question', 'type': 'string'}}
|
||||
assert converted_tool.func("What is the meaning of life?") == "What is the meaning of life?"
|
||||
|
||||
def test_creating_a_tool_using_baseclass():
|
||||
class MyCustomTool(BaseTool):
|
||||
name: str = "Name of my tool"
|
||||
description: str = "Clear description for what this tool is useful for, you agent will need this information to use it."
|
||||
|
||||
def _run(self, question: str) -> str:
|
||||
return question
|
||||
|
||||
my_tool = MyCustomTool()
|
||||
# Assert all the right attributes were defined
|
||||
assert my_tool.name == "Name of my tool"
|
||||
assert my_tool.description == "Clear description for what this tool is useful for, you agent will need this information to use it."
|
||||
assert my_tool.args_schema.schema()["properties"] == {'question': {'title': 'Question', 'type': 'string'}}
|
||||
assert my_tool.run("What is the meaning of life?") == "What is the meaning of life?"
|
||||
|
||||
# Assert the langchain tool conversion worked as expected
|
||||
converted_tool = my_tool.to_langchain()
|
||||
assert converted_tool.name == "Name of my tool"
|
||||
assert converted_tool.description == "Clear description for what this tool is useful for, you agent will need this information to use it."
|
||||
assert converted_tool.args_schema.schema()["properties"] == {'question': {'title': 'Question', 'type': 'string'}}
|
||||
assert converted_tool.run("What is the meaning of life?") == "What is the meaning of life?"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +0,0 @@
|
||||
$d2c46569-d173-4b3f-b589-f8f00eddc371<37>Vtext <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>*string085vector <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>*fixed_size_list:float:153608
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,21 +0,0 @@
|
||||
from crewai_tools.tools.rag.rag_tool import Adapter, RagTool
|
||||
|
||||
|
||||
class MockAdapter(Adapter):
|
||||
answer: str
|
||||
|
||||
def query(self, question: str) -> str:
|
||||
return self.answer
|
||||
|
||||
|
||||
def test_rag_tool():
|
||||
adapter = MockAdapter(answer="42")
|
||||
rag_tool = RagTool(adapter=adapter)
|
||||
|
||||
assert rag_tool.name == "Knowledge base"
|
||||
assert (
|
||||
rag_tool.description == "A knowledge base that can be used to answer questions."
|
||||
)
|
||||
assert (
|
||||
rag_tool.run("What is the answer to life, the universe and everything?") == "42"
|
||||
)
|
||||
Reference in New Issue
Block a user